This commit is contained in:
Ryan VanderMeulen 2014-08-20 15:56:32 -04:00
Родитель ff85627591 7fe81216dd
Коммит 6addb46104
64 изменённых файлов: 1792 добавлений и 435 удалений

Просмотреть файл

@ -9,6 +9,21 @@ var FullScreen = {
delete this._fullScrToggler;
return this._fullScrToggler = document.getElementById("fullscr-toggler");
},
init: function() {
// called when we go into full screen, even if initiated by a web page script
window.addEventListener("fullscreen", this, true);
window.messageManager.addMessageListener("MozEnteredDomFullscreen", this);
if (window.fullScreen)
this.toggle();
},
uninit: function() {
window.messageManager.removeMessageListener("MozEnteredDomFullscreen", this);
this.cleanup();
},
toggle: function (event) {
var enterFS = window.fullScreen;
@ -95,9 +110,12 @@ var FullScreen = {
switch (event.type) {
case "activate":
if (document.mozFullScreen) {
this.showWarning(this.fullscreenDoc);
this.showWarning(this.fullscreenOrigin);
}
break;
case "fullscreen":
this.toggle(event);
break;
case "transitionend":
if (event.propertyName == "opacity")
this.cancelWarning();
@ -105,18 +123,33 @@ var FullScreen = {
}
},
enterDomFullscreen : function(event) {
receiveMessage: function(aMessage) {
if (aMessage.name == "MozEnteredDomFullscreen") {
// If we're a multiprocess browser, then the request to enter fullscreen
// did not bubble up to the root browser document - it stopped at the root
// of the content document. That means we have to kick off the switch to
// fullscreen here at the operating system level in the parent process
// ourselves.
let data = aMessage.data;
let browser = aMessage.target;
if (gMultiProcessBrowser && browser.getAttribute("remote") == "true") {
let windowUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
windowUtils.remoteFrameFullscreenChanged(browser, data.origin);
}
this.enterDomFullscreen(browser, data.origin);
}
},
enterDomFullscreen : function(aBrowser, aOrigin) {
if (!document.mozFullScreen)
return;
// However, if we receive a "MozEnteredDomFullScreen" event for a document
// which is not a subdocument of a currently active (ie. visible) browser
// or iframe, we know that we've switched to a different frame since the
// request to enter full-screen was made, so we should exit full-screen
// since the "full-screen document" isn't acutally visible.
if (!event.target.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell).isActive) {
// If we've received a fullscreen notification, we have to ensure that the
// element that's requesting fullscreen belongs to the browser that's currently
// active. If not, we exit fullscreen since the "full-screen document" isn't
// actually visible now.
if (gBrowser.selectedBrowser != aBrowser) {
document.mozCancelFullScreen();
return;
}
@ -136,7 +169,7 @@ var FullScreen = {
if (gFindBarInitialized)
gFindBar.close();
this.showWarning(event.target);
this.showWarning(aOrigin);
// Exit DOM full-screen mode upon open, close, or change tab.
gBrowser.tabContainer.addEventListener("TabOpen", this.exitDomFullScreen);
@ -178,7 +211,9 @@ var FullScreen = {
gBrowser.tabContainer.removeEventListener("TabSelect", this.exitDomFullScreen);
if (!this.useLionFullScreen)
window.removeEventListener("activate", this);
this.fullscreenDoc = null;
window.messageManager
.broadcastAsyncMessage("DOMFullscreen:Cleanup");
}
},
@ -337,7 +372,7 @@ var FullScreen = {
// the permission manager can't handle (documents with URIs without a host).
// We simply require those to be approved every time instead.
let rememberCheckbox = document.getElementById("full-screen-remember-decision");
let uri = this.fullscreenDoc.nodePrincipal.URI;
let uri = BrowserUtils.makeURI(this.fullscreenOrigin);
if (!rememberCheckbox.hidden) {
if (rememberCheckbox.checked)
Services.perms.add(uri,
@ -370,27 +405,29 @@ var FullScreen = {
// If the document has been granted fullscreen, notify Gecko so it can resume
// any pending pointer lock requests, otherwise exit fullscreen; the user denied
// the fullscreen request.
if (isApproved)
Services.obs.notifyObservers(this.fullscreenDoc, "fullscreen-approved", "");
else
if (isApproved) {
gBrowser.selectedBrowser
.messageManager
.sendAsyncMessage("DOMFullscreen:Approved");
} else {
document.mozCancelFullScreen();
}
},
warningBox: null,
warningFadeOutTimeout: null,
fullscreenDoc: null,
// Shows the fullscreen approval UI, or if the domain has already been approved
// for fullscreen, shows a warning that the site has entered fullscreen for a short
// duration.
showWarning: function(targetDoc) {
showWarning: function(aOrigin) {
if (!document.mozFullScreen ||
!gPrefService.getBoolPref("full-screen-api.approval-required"))
return;
// Set the strings on the fullscreen approval UI.
this.fullscreenDoc = targetDoc;
let uri = this.fullscreenDoc.nodePrincipal.URI;
this.fullscreenOrigin = aOrigin;
let uri = BrowserUtils.makeURI(aOrigin);
let host = null;
try {
host = uri.host;

Просмотреть файл

@ -23,6 +23,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils",
"resource://gre/modules/ShortcutUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "GMPInstallManager",
"resource://gre/modules/GMPInstallManager.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NewTabUtils",
"resource://gre/modules/NewTabUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ContentSearch",
"resource:///modules/ContentSearch.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AboutHome",
@ -1294,17 +1296,7 @@ var gBrowserInit = {
if (Win7Features)
Win7Features.onOpenWindow();
// called when we go into full screen, even if initiated by a web page script
window.addEventListener("fullscreen", onFullScreen, true);
// Called when we enter DOM full-screen mode. Note we can already be in browser
// full-screen mode when we enter DOM full-screen mode.
window.addEventListener("MozEnteredDomFullscreen", onMozEnteredDomFullscreen, true);
if (window.fullScreen)
onFullScreen();
if (document.mozFullScreen)
onMozEnteredDomFullscreen();
FullScreen.init();
#ifdef MOZ_SERVICES_SYNC
// initialize the sync UI
@ -1435,7 +1427,7 @@ var gBrowserInit = {
gHistorySwipeAnimation.uninit();
FullScreen.cleanup();
FullScreen.uninit();
#ifdef MOZ_SERVICES_SYNC
gFxAccounts.uninit();
@ -2762,14 +2754,6 @@ function SwitchToMetro() {
#endif
}
function onFullScreen(event) {
FullScreen.toggle(event);
}
function onMozEnteredDomFullscreen(event) {
FullScreen.enterDomFullscreen(event);
}
function getWebNavigation()
{
return gBrowser.webNavigation;
@ -3109,7 +3093,7 @@ const BrowserSearch = {
let mm = gBrowser.selectedBrowser.messageManager;
if (url === "about:home") {
AboutHome.focusInput(mm);
} else if (url === "about:newtab") {
} else if (url === "about:newtab" && NewTabUtils.allPages.enabled) {
ContentSearch.focusInput(mm);
} else {
openUILinkIn("about:home", "current");

Просмотреть файл

@ -578,3 +578,40 @@ if (Services.prefs.getBoolPref("browser.translation.detectLanguage")) {
Cu.import("resource:///modules/translation/TranslationContentHandler.jsm");
trHandler = new TranslationContentHandler(global, docShell);
}
let DOMFullscreenHandler = {
_fullscreenDoc: null,
init: function() {
addMessageListener("DOMFullscreen:Approved", this);
addMessageListener("DOMFullscreen:CleanUp", this);
addEventListener("MozEnteredDomFullscreen", this);
},
receiveMessage: function(aMessage) {
switch(aMessage.name) {
case "DOMFullscreen:Approved": {
if (this._fullscreenDoc) {
Services.obs.notifyObservers(this._fullscreenDoc,
"fullscreen-approved",
"");
}
break;
}
case "DOMFullscreen:CleanUp": {
this._fullscreenDoc = null;
break;
}
}
},
handleEvent: function(aEvent) {
if (aEvent.type == "MozEnteredDomFullscreen") {
this._fullscreenDoc = aEvent.target;
sendAsyncMessage("MozEnteredDomFullscreen", {
origin: this._fullscreenDoc.nodePrincipal.origin,
});
}
}
};
DOMFullscreenHandler.init();

Просмотреть файл

@ -1225,6 +1225,15 @@
// If we're using remote tabs, we have to wait until after we've finalized
// switching the tabs.
if (newTab._skipContentFocus) {
// It's possible the tab we're switching to is ready to focus asynchronously,
// when we've already focused something else. In that case, this
// _skipContentFocus property can be set so that we skip focusing the
// content after we switch tabs.
delete newTab._skipContentFocus;
return;
}
let fm = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
let focusFlags = fm.FLAG_NOSCROLL;

Просмотреть файл

@ -205,6 +205,24 @@ function runTests() {
EventUtils.synthesizeKey("k", { accelKey: true });
is(searchBar.textbox.inputField, gWindow.document.activeElement, "Toolbar's search bar should be focused");
// Test that Ctrl/Cmd + K will focus the search bar from a new about:home page if
// the newtab is disabled from `NewTabUtils.allPages.enabled`.
yield addNewTabPageTab();
// Remove the search bar from toolbar
CustomizableUI.removeWidgetFromArea("search-container");
NewTabUtils.allPages.enabled = false;
EventUtils.synthesizeKey("k", { accelKey: true });
let waitEvent = "AboutHomeLoadSnippetsCompleted";
yield promiseTabLoadEvent(gWindow.gBrowser.selectedTab, "about:home", waitEvent).then(TestRunner.next);
is(getContentDocument().documentURI.toLowerCase(), "about:home", "New tab's uri should be about:home");
let searchInput = getContentDocument().getElementById("searchText");
is(searchInput, getContentDocument().activeElement, "Search input must be the selected element");
NewTabUtils.allPages.enabled = true;
CustomizableUI.reset();
gBrowser.removeCurrentTab();
// Done. Revert the current engine and remove the new engines.
Services.search.currentEngine = oldCurrentEngine;
yield promiseSearchEvents(["CurrentEngine"]).then(TestRunner.next);
@ -414,3 +432,46 @@ function logoImg() {
function gSearch() {
return getContentWindow().gSearch;
}
/**
* Waits for a load (or custom) event to finish in a given tab. If provided
* load an uri into the tab.
*
* @param tab
* The tab to load into.
* @param [optional] url
* The url to load, or the current url.
* @param [optional] event
* The load event type to wait for. Defaults to "load".
* @return {Promise} resolved when the event is handled.
* @resolves to the received event
* @rejects if a valid load event is not received within a meaningful interval
*/
function promiseTabLoadEvent(tab, url, eventType="load") {
let deferred = Promise.defer();
info("Wait tab event: " + eventType);
function handle(event) {
if (event.originalTarget != tab.linkedBrowser.contentDocument ||
event.target.location.href == "about:blank" ||
(url && event.target.location.href != url)) {
info("Skipping spurious '" + eventType + "'' event" +
" for " + event.target.location.href);
return;
}
clearTimeout(timeout);
tab.linkedBrowser.removeEventListener(eventType, handle, true);
info("Tab event received: " + eventType);
deferred.resolve(event);
}
let timeout = setTimeout(() => {
tab.linkedBrowser.removeEventListener(eventType, handle, true);
deferred.reject(new Error("Timed out while waiting for a '" + eventType + "'' event"));
}, 20000);
tab.linkedBrowser.addEventListener(eventType, handle, true, true);
if (url)
tab.linkedBrowser.loadURI(url);
return deferred.promise;
}

Просмотреть файл

@ -14,7 +14,7 @@ Cc["@mozilla.org/moz/jssubscript-loader;1"]
.getService(Ci.mozIJSSubScriptLoader)
.loadSubScript("chrome://browser/content/sanitize.js", tmp);
Cu.import("resource://gre/modules/Timer.jsm", tmp);
let {Promise, NewTabUtils, Sanitizer, clearTimeout, DirectoryLinksProvider} = tmp;
let {Promise, NewTabUtils, Sanitizer, clearTimeout, setTimeout, DirectoryLinksProvider} = tmp;
let uri = Services.io.newURI("about:newtab", null, null);
let principal = Services.scriptSecurityManager.getNoAppCodebasePrincipal(uri);

Просмотреть файл

@ -309,6 +309,8 @@ function openLinkIn(url, where, params) {
// result in a new frontmost window (e.g. "javascript:window.open('');").
w.focus();
let newTab;
switch (where) {
case "current":
let flags = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
@ -331,23 +333,30 @@ function openLinkIn(url, where, params) {
loadInBackground = !loadInBackground;
// fall through
case "tab":
let browser = w.gBrowser;
browser.loadOneTab(url, {
referrerURI: aReferrerURI,
charset: aCharset,
postData: aPostData,
inBackground: loadInBackground,
allowThirdPartyFixup: aAllowThirdPartyFixup,
relatedToCurrent: aRelatedToCurrent,
skipAnimation: aSkipTabAnimation,
allowMixedContent: aAllowMixedContent });
newTab = w.gBrowser.loadOneTab(url, {
referrerURI: aReferrerURI,
charset: aCharset,
postData: aPostData,
inBackground: loadInBackground,
allowThirdPartyFixup: aAllowThirdPartyFixup,
relatedToCurrent: aRelatedToCurrent,
skipAnimation: aSkipTabAnimation,
allowMixedContent: aAllowMixedContent
});
break;
}
w.gBrowser.selectedBrowser.focus();
if (!loadInBackground && w.isBlankPageURL(url))
if (!loadInBackground && w.isBlankPageURL(url)) {
if (newTab && gMultiProcessBrowser) {
// Remote browsers are switched to asynchronously, and we need to
// ensure that the location bar remains focused in that case rather
// than the content area being focused.
newTab._skipContentFocus = true;
}
w.focusAndSelectUrlBar();
}
}
// Used as an onclick handler for UI elements with link-like behavior.

Просмотреть файл

@ -7,24 +7,75 @@ this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/Chr
XPCOMUtils.defineLazyModuleGetter(this, "FormHistory",
"resource://gre/modules/FormHistory.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
function test() {
waitForExplicitFinish();
function expectedURL(aSearchTerms) {
const ENGINE_HTML_BASE = "http://mochi.test:8888/browser/browser/components/search/test/test.html";
var textToSubURI = Cc["@mozilla.org/intl/texttosuburi;1"].
getService(Ci.nsITextToSubURI);
var searchArg = textToSubURI.ConvertAndEscape("utf-8", aSearchTerms);
return ENGINE_HTML_BASE + "?test=" + searchArg;
}
var searchEntries = ["test", "More Text", "Some Text"];
var searchBar = BrowserSearch.searchBar;
var searchButton = document.getAnonymousElementByAttribute(searchBar,
"anonid", "search-go-button");
ok(searchButton, "got search-go-button");
function simulateClick(aEvent, aTarget) {
var event = document.createEvent("MouseEvent");
var ctrlKeyArg = aEvent.ctrlKey || false;
var altKeyArg = aEvent.altKey || false;
var shiftKeyArg = aEvent.shiftKey || false;
var metaKeyArg = aEvent.metaKey || false;
var buttonArg = aEvent.button || 0;
event.initMouseEvent("click", true, true, window,
0, 0, 0, 0, 0,
ctrlKeyArg, altKeyArg, shiftKeyArg, metaKeyArg,
buttonArg, null);
aTarget.dispatchEvent(event);
}
searchBar.value = "test";
// modified from toolkit/components/satchel/test/test_form_autocomplete.html
function checkMenuEntries(expectedValues) {
var actualValues = getMenuEntries();
is(actualValues.length, expectedValues.length, "Checking length of expected menu");
for (var i = 0; i < expectedValues.length; i++)
is(actualValues[i], expectedValues[i], "Checking menu entry #" + i);
}
function getMenuEntries() {
var entries = [];
var autocompleteMenu = searchBar.textbox.popup;
// Could perhaps pull values directly from the controller, but it seems
// more reliable to test the values that are actually in the tree?
var column = autocompleteMenu.tree.columns[0];
var numRows = autocompleteMenu.tree.view.rowCount;
for (var i = 0; i < numRows; i++) {
entries.push(autocompleteMenu.tree.view.getValueAt(i, column));
}
return entries;
}
function* countEntries(name, value) {
let deferred = Promise.defer();
let count = 0;
let obj = name && value ? {fieldname: name, value: value} : {};
FormHistory.count(obj,
{ handleResult: function(result) { count = result; },
handleError: function(error) { throw error; },
handleCompletion: function(reason) {
if (!reason) {
deferred.resolve(count);
}
}
});
return deferred.promise;
}
var searchBar;
var searchButton;
var searchEntries = ["test", "More Text", "Some Text"];
function* promiseSetEngine() {
let deferred = Promise.defer();
var ss = Services.search;
let testIterator;
function observer(aSub, aTopic, aData) {
switch (aData) {
case "engine-added":
@ -34,266 +85,221 @@ function test() {
break;
case "engine-current":
ok(ss.currentEngine.name == "Bug 426329", "currentEngine set");
testReturn();
break;
case "engine-removed":
searchBar = BrowserSearch.searchBar;
searchButton = document.getAnonymousElementByAttribute(searchBar,
"anonid", "search-go-button");
ok(searchButton, "got search-go-button");
searchBar.value = "test";
Services.obs.removeObserver(observer, "browser-search-engine-modified");
finish();
deferred.resolve();
break;
}
}
};
Services.obs.addObserver(observer, "browser-search-engine-modified", false);
ss.addEngine("http://mochi.test:8888/browser/browser/components/search/test/426329.xml",
Ci.nsISearchEngine.DATA_XML, "data:image/x-icon,%00",
false);
var preSelectedBrowser, preTabNo;
function init() {
preSelectedBrowser = gBrowser.selectedBrowser;
preTabNo = gBrowser.tabs.length;
searchBar.focus();
}
function testReturn() {
init();
EventUtils.synthesizeKey("VK_RETURN", {});
doOnloadOnce(function(event) {
is(gBrowser.tabs.length, preTabNo, "Return key did not open new tab");
is(event.originalTarget, preSelectedBrowser.contentDocument,
"Return key loaded results in current tab");
is(event.originalTarget.URL, expectedURL(searchBar.value), "testReturn opened correct search page");
testAltReturn();
});
}
function testAltReturn() {
init();
EventUtils.synthesizeKey("VK_RETURN", { altKey: true });
doOnloadOnce(function(event) {
is(gBrowser.tabs.length, preTabNo + 1, "Alt+Return key added new tab");
isnot(event.originalTarget, preSelectedBrowser.contentDocument,
"Alt+Return key loaded results in new tab");
is(event.originalTarget, gBrowser.contentDocument,
"Alt+Return key loaded results in foreground tab");
is(event.originalTarget.URL, expectedURL(searchBar.value), "testAltReturn opened correct search page");
//Shift key has no effect for now, so skip it
//testShiftAltReturn();
testLeftClick();
});
}
function testShiftAltReturn() {
init();
EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true, altKey: true });
doOnloadOnce(function(event) {
is(gBrowser.tabs.length, preTabNo + 1, "Shift+Alt+Return key added new tab");
isnot(event.originalTarget, preSelectedBrowser.contentDocument,
"Shift+Alt+Return key loaded results in new tab");
isnot(event.originalTarget, gBrowser.contentDocument,
"Shift+Alt+Return key loaded results in background tab");
is(event.originalTarget.URL, expectedURL(searchBar.value), "testShiftAltReturn opened correct search page");
testLeftClick();
});
}
function testLeftClick() {
init();
simulateClick({ button: 0 }, searchButton);
doOnloadOnce(function(event) {
is(gBrowser.tabs.length, preTabNo, "LeftClick did not open new tab");
is(event.originalTarget, preSelectedBrowser.contentDocument,
"LeftClick loaded results in current tab");
is(event.originalTarget.URL, expectedURL(searchBar.value), "testLeftClick opened correct search page");
testMiddleClick();
});
}
function testMiddleClick() {
init();
simulateClick({ button: 1 }, searchButton);
doOnloadOnce(function(event) {
is(gBrowser.tabs.length, preTabNo + 1, "MiddleClick added new tab");
isnot(event.originalTarget, preSelectedBrowser.contentDocument,
"MiddleClick loaded results in new tab");
is(event.originalTarget, gBrowser.contentDocument,
"MiddleClick loaded results in foreground tab");
is(event.originalTarget.URL, expectedURL(searchBar.value), "testMiddleClick opened correct search page");
testShiftMiddleClick();
});
}
function testShiftMiddleClick() {
init();
simulateClick({ button: 1, shiftKey: true }, searchButton);
doOnloadOnce(function(event) {
is(gBrowser.tabs.length, preTabNo + 1, "Shift+MiddleClick added new tab");
isnot(event.originalTarget, preSelectedBrowser.contentDocument,
"Shift+MiddleClick loaded results in new tab");
isnot(event.originalTarget, gBrowser.contentDocument,
"Shift+MiddleClick loaded results in background tab");
is(event.originalTarget.URL, expectedURL(searchBar.value), "testShiftMiddleClick opened correct search page");
testDropText();
});
}
// prevent the search buttonmenu from opening during the drag tests
function stopPopup(event) { event.preventDefault(); }
function testDropText() {
init();
searchBar.addEventListener("popupshowing", stopPopup, true);
// drop on the search button so that we don't need to worry about the
// default handlers for textboxes.
ChromeUtils.synthesizeDrop(searchBar.searchButton, searchBar.searchButton, [[ {type: "text/plain", data: "Some Text" } ]], "copy", window);
doOnloadOnce(function(event) {
is(searchBar.value, "Some Text", "drop text/plain on searchbar");
testDropInternalText();
});
}
function testDropInternalText() {
init();
ChromeUtils.synthesizeDrop(searchBar.searchButton, searchBar.searchButton, [[ {type: "text/x-moz-text-internal", data: "More Text" } ]], "copy", window);
doOnloadOnce(function(event) {
is(searchBar.value, "More Text", "drop text/x-moz-text-internal on searchbar");
testDropLink();
});
}
function testDropLink() {
init();
ChromeUtils.synthesizeDrop(searchBar.searchButton, searchBar.searchButton, [[ {type: "text/uri-list", data: "http://www.mozilla.org" } ]], "copy", window);
is(searchBar.value, "More Text", "drop text/uri-list on searchbar");
SimpleTest.executeSoon(testRightClick);
}
function testRightClick() {
init();
searchBar.removeEventListener("popupshowing", stopPopup, true);
content.location.href = "about:blank";
simulateClick({ button: 2 }, searchButton);
setTimeout(function() {
is(gBrowser.tabs.length, preTabNo, "RightClick did not open new tab");
is(gBrowser.currentURI.spec, "about:blank", "RightClick did nothing");
testIterator = testSearchHistory();
testIterator.next();
}, 5000);
}
function countEntries(name, value, message) {
let count = 0;
FormHistory.count({ fieldname: name, value: value },
{ handleResult: function(result) { count = result; },
handleError: function(error) { throw error; },
handleCompletion: function(reason) {
if (!reason) {
ok(count > 0, message);
testIterator.next();
}
}
});
}
function testSearchHistory() {
var textbox = searchBar._textbox;
for (var i = 0; i < searchEntries.length; i++) {
yield countEntries(textbox.getAttribute("autocompletesearchparam"), searchEntries[i],
"form history entry '" + searchEntries[i] + "' should exist");
}
testAutocomplete();
}
function testAutocomplete() {
var popup = searchBar.textbox.popup;
popup.addEventListener("popupshown", function testACPopupShowing() {
popup.removeEventListener("popupshown", testACPopupShowing);
checkMenuEntries(searchEntries);
testClearHistory();
});
searchBar.textbox.showHistoryPopup();
}
function testClearHistory() {
let controller = searchBar.textbox.controllers.getControllerForCommand("cmd_clearhistory")
ok(controller.isCommandEnabled("cmd_clearhistory"), "Clear history command enabled");
controller.doCommand("cmd_clearhistory");
let count = 0;
FormHistory.count({ },
{ handleResult: function(result) { count = result; },
handleError: function(error) { throw error; },
handleCompletion: function(reason) {
if (!reason) {
ok(count == 0, "History cleared");
finalize();
}
}
});
}
function finalize() {
searchBar.value = "";
while (gBrowser.tabs.length != 1) {
gBrowser.removeTab(gBrowser.tabs[0]);
}
content.location.href = "about:blank";
var engine = ss.getEngineByName("Bug 426329");
ss.removeEngine(engine);
}
function simulateClick(aEvent, aTarget) {
var event = document.createEvent("MouseEvent");
var ctrlKeyArg = aEvent.ctrlKey || false;
var altKeyArg = aEvent.altKey || false;
var shiftKeyArg = aEvent.shiftKey || false;
var metaKeyArg = aEvent.metaKey || false;
var buttonArg = aEvent.button || 0;
event.initMouseEvent("click", true, true, window,
0, 0, 0, 0, 0,
ctrlKeyArg, altKeyArg, shiftKeyArg, metaKeyArg,
buttonArg, null);
aTarget.dispatchEvent(event);
}
function expectedURL(aSearchTerms) {
var textToSubURI = Cc["@mozilla.org/intl/texttosuburi;1"].
getService(Ci.nsITextToSubURI);
var searchArg = textToSubURI.ConvertAndEscape("utf-8", aSearchTerms);
return ENGINE_HTML_BASE + "?test=" + searchArg;
}
// modified from toolkit/components/satchel/test/test_form_autocomplete.html
function checkMenuEntries(expectedValues) {
var actualValues = getMenuEntries();
is(actualValues.length, expectedValues.length, "Checking length of expected menu");
for (var i = 0; i < expectedValues.length; i++)
is(actualValues[i], expectedValues[i], "Checking menu entry #" + i);
}
function getMenuEntries() {
var entries = [];
var autocompleteMenu = searchBar.textbox.popup;
// Could perhaps pull values directly from the controller, but it seems
// more reliable to test the values that are actually in the tree?
var column = autocompleteMenu.tree.columns[0];
var numRows = autocompleteMenu.tree.view.rowCount;
for (var i = 0; i < numRows; i++) {
entries.push(autocompleteMenu.tree.view.getValueAt(i, column));
}
return entries;
}
return deferred.promise;
}
function* promiseRemoveEngine() {
let deferred = Promise.defer();
var ss = Services.search;
function observer(aSub, aTopic, aData) {
if (aData == "engine-removed") {
Services.obs.removeObserver(observer, "browser-search-engine-modified");
deferred.resolve();
}
};
Services.obs.addObserver(observer, "browser-search-engine-modified", false);
var engine = ss.getEngineByName("Bug 426329");
ss.removeEngine(engine);
return deferred.promise;
}
var preSelectedBrowser;
var preTabNo;
function* prepareTest() {
preSelectedBrowser = gBrowser.selectedBrowser;
preTabNo = gBrowser.tabs.length;
searchBar = BrowserSearch.searchBar;
let windowFocused = Promise.defer();
SimpleTest.waitForFocus(windowFocused.resolve, window);
yield windowFocused.promise;
let deferred = Promise.defer();
if (document.activeElement != searchBar) {
searchBar.addEventListener("focus", function onFocus() {
searchBar.removeEventListener("focus", onFocus);
deferred.resolve();
});
searchBar.focus();
} else {
deferred.resolve();
}
return deferred.promise;
}
add_task(function testSetupEngine() {
yield promiseSetEngine();
});
add_task(function testReturn() {
yield prepareTest();
EventUtils.synthesizeKey("VK_RETURN", {});
let event = yield promiseOnLoad();
is(gBrowser.tabs.length, preTabNo, "Return key did not open new tab");
is(event.originalTarget, preSelectedBrowser.contentDocument,
"Return key loaded results in current tab");
is(event.originalTarget.URL, expectedURL(searchBar.value), "testReturn opened correct search page");
});
add_task(function testAltReturn() {
yield prepareTest();
EventUtils.synthesizeKey("VK_RETURN", { altKey: true });
let event = yield promiseOnLoad();
is(gBrowser.tabs.length, preTabNo + 1, "Alt+Return key added new tab");
isnot(event.originalTarget, preSelectedBrowser.contentDocument,
"Alt+Return key loaded results in new tab");
is(event.originalTarget, gBrowser.contentDocument,
"Alt+Return key loaded results in foreground tab");
is(event.originalTarget.URL, expectedURL(searchBar.value), "testAltReturn opened correct search page");
});
//Shift key has no effect for now, so skip it
add_task(function testShiftAltReturn() {
return;
yield prepareTest();
EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true, altKey: true });
let event = yield promiseOnLoad();
is(gBrowser.tabs.length, preTabNo + 1, "Shift+Alt+Return key added new tab");
isnot(event.originalTarget, preSelectedBrowser.contentDocument,
"Shift+Alt+Return key loaded results in new tab");
isnot(event.originalTarget, gBrowser.contentDocument,
"Shift+Alt+Return key loaded results in background tab");
is(event.originalTarget.URL, expectedURL(searchBar.value), "testShiftAltReturn opened correct search page");
});
add_task(function testLeftClick() {
yield prepareTest();
simulateClick({ button: 0 }, searchButton);
let event = yield promiseOnLoad();
is(gBrowser.tabs.length, preTabNo, "LeftClick did not open new tab");
is(event.originalTarget, preSelectedBrowser.contentDocument,
"LeftClick loaded results in current tab");
is(event.originalTarget.URL, expectedURL(searchBar.value), "testLeftClick opened correct search page");
});
add_task(function testMiddleClick() {
yield prepareTest();
simulateClick({ button: 1 }, searchButton);
let event = yield promiseOnLoad();
is(gBrowser.tabs.length, preTabNo + 1, "MiddleClick added new tab");
isnot(event.originalTarget, preSelectedBrowser.contentDocument,
"MiddleClick loaded results in new tab");
is(event.originalTarget, gBrowser.contentDocument,
"MiddleClick loaded results in foreground tab");
is(event.originalTarget.URL, expectedURL(searchBar.value), "testMiddleClick opened correct search page");
});
add_task(function testShiftMiddleClick() {
yield prepareTest();
simulateClick({ button: 1, shiftKey: true }, searchButton);
let event = yield promiseOnLoad();
is(gBrowser.tabs.length, preTabNo + 1, "Shift+MiddleClick added new tab");
isnot(event.originalTarget, preSelectedBrowser.contentDocument,
"Shift+MiddleClick loaded results in new tab");
isnot(event.originalTarget, gBrowser.contentDocument,
"Shift+MiddleClick loaded results in background tab");
is(event.originalTarget.URL, expectedURL(searchBar.value), "testShiftMiddleClick opened correct search page");
});
add_task(function testDropText() {
yield prepareTest();
let promisePreventPopup = promiseEvent(searchBar, "popupshowing", true);
// drop on the search button so that we don't need to worry about the
// default handlers for textboxes.
ChromeUtils.synthesizeDrop(searchBar.searchButton, searchBar.searchButton, [[ {type: "text/plain", data: "Some Text" } ]], "copy", window);
yield promisePreventPopup;
let event = yield promiseOnLoad();
is(event.originalTarget.URL, expectedURL(searchBar.value), "testDropText opened correct search page");
is(searchBar.value, "Some Text", "drop text/plain on searchbar");
});
add_task(function testDropInternalText() {
yield prepareTest();
let promisePreventPopup = promiseEvent(searchBar, "popupshowing", true);
ChromeUtils.synthesizeDrop(searchBar.searchButton, searchBar.searchButton, [[ {type: "text/x-moz-text-internal", data: "More Text" } ]], "copy", window);
yield promisePreventPopup;
let event = yield promiseOnLoad();
is(event.originalTarget.URL, expectedURL(searchBar.value), "testDropInternalText opened correct search page");
is(searchBar.value, "More Text", "drop text/x-moz-text-internal on searchbar");
// testDropLink implicitly depended on testDropInternalText, so these two tests
// were merged so that if testDropInternalText failed it wouldn't cause testDropLink
// to fail unexplainably.
yield prepareTest();
let promisePreventPopup = promiseEvent(searchBar, "popupshowing", true);
ChromeUtils.synthesizeDrop(searchBar.searchButton, searchBar.searchButton, [[ {type: "text/uri-list", data: "http://www.mozilla.org" } ]], "copy", window);
yield promisePreventPopup;
is(searchBar.value, "More Text", "drop text/uri-list on searchbar shouldn't change anything");
});
add_task(function testRightClick() {
preTabNo = gBrowser.tabs.length;
content.location.href = "about:blank";
simulateClick({ button: 2 }, searchButton);
let deferred = Promise.defer();
setTimeout(function() {
is(gBrowser.tabs.length, preTabNo, "RightClick did not open new tab");
is(gBrowser.currentURI.spec, "about:blank", "RightClick did nothing");
deferred.resolve();
}, 5000);
yield deferred.promise;
});
add_task(function testSearchHistory() {
var textbox = searchBar._textbox;
for (var i = 0; i < searchEntries.length; i++) {
let count = yield countEntries(textbox.getAttribute("autocompletesearchparam"), searchEntries[i]);
ok(count > 0, "form history entry '" + searchEntries[i] + "' should exist");
}
});
add_task(function testAutocomplete() {
var popup = searchBar.textbox.popup;
let popupShownPromise = promiseEvent(popup, "popupshown");
searchBar.textbox.showHistoryPopup();
yield popupShownPromise;
checkMenuEntries(searchEntries);
});
add_task(function testClearHistory() {
let controller = searchBar.textbox.controllers.getControllerForCommand("cmd_clearhistory")
ok(controller.isCommandEnabled("cmd_clearhistory"), "Clear history command enabled");
controller.doCommand("cmd_clearhistory");
let count = yield countEntries();
ok(count == 0, "History cleared");
});
add_task(function asyncCleanup() {
searchBar.value = "";
while (gBrowser.tabs.length != 1) {
gBrowser.removeTab(gBrowser.tabs[0], {animate: false});
}
content.location.href = "about:blank";
yield promiseRemoveEngine();
});

Просмотреть файл

@ -1,6 +1,9 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
function whenNewWindowLoaded(aOptions, aCallback) {
let win = OpenBrowserWindow(aOptions);
let gotLoad = false;
@ -88,6 +91,18 @@ function waitForPopupShown(aPopupId, aCallback) {
registerCleanupFunction(removePopupShownListener);
}
function* promiseEvent(aTarget, aEventName, aPreventDefault) {
let deferred = Promise.defer();
aTarget.addEventListener(aEventName, function onEvent(aEvent) {
aTarget.removeEventListener(aEventName, onEvent, true);
if (aPreventDefault) {
aEvent.preventDefault();
}
deferred.resolve();
}, true);
return deferred.promise;
}
function waitForBrowserContextMenu(aCallback) {
waitForPopupShown(gBrowser.selectedBrowser.contextMenu, aCallback);
}
@ -106,3 +121,16 @@ function doOnloadOnce(aCallback) {
gBrowser.addEventListener("load", doOnloadOnceListener, true);
registerCleanupFunction(removeDoOnloadOnceListener);
}
function* promiseOnLoad() {
let deferred = Promise.defer();
gBrowser.addEventListener("load", function onLoadListener(aEvent) {
info("onLoadListener: " + aEvent.originalTarget.location);
gBrowser.removeEventListener("load", onLoadListener, true);
deferred.resolve(aEvent);
}, true);
return deferred.promise;
}

Просмотреть файл

@ -22,13 +22,13 @@ const EventEmitter = devtools.require("devtools/toolkit/event-emitter");
const FORBIDDEN_IDS = new Set(["toolbox", ""]);
const MAX_ORDINAL = 99;
/**
* DevTools is a class that represents a set of developer tools, it holds a
* set of tools and keeps track of open toolboxes in the browser.
*/
this.DevTools = function DevTools() {
this._tools = new Map(); // Map<toolId, tool>
this._themes = new Map(); // Map<themeId, theme>
this._toolboxes = new Map(); // Map<target, toolbox>
// destroy() is an observer's handler so we need to preserve context.
@ -229,6 +229,136 @@ DevTools.prototype = {
return definitions.sort(this.ordinalSort);
},
/**
* Register a new theme for developer tools toolbox.
*
* A definition is a light object that holds various information about a
* theme.
*
* Each themeDefinition has the following properties:
* - id: Unique identifier for this theme (string|required)
* - label: Localized name for the theme to be displayed to the user
* (string|required)
* - stylesheets: Array of URLs pointing to a CSS document(s) containing
* the theme style rules (array|required)
* - classList: Array of class names identifying the theme within a document.
* These names are set to document element when applying
* the theme (array|required)
* - onApply: Function that is executed by the framework when the theme
* is applied. The function takes the current iframe window
* and the previous theme id as arguments (function)
* - onUnapply: Function that is executed by the framework when the theme
* is unapplied. The function takes the current iframe window
* and the new theme id as arguments (function)
*/
registerTheme: function DT_registerTheme(themeDefinition) {
let themeId = themeDefinition.id;
if (!themeId) {
throw new Error("Invalid theme id");
}
if (this._themes.get(themeId)) {
throw new Error("Theme with the same id is already registered");
}
this._themes.set(themeId, themeDefinition);
this.emit("theme-registered", themeId);
},
/**
* Removes an existing theme from the list of registered themes.
* Needed so that add-ons can remove themselves when they are deactivated
*
* @param {string|object} theme
* Definition or the id of the theme to unregister.
*/
unregisterTheme: function DT_unregisterTheme(theme) {
let themeId = null;
if (typeof theme == "string") {
themeId = theme;
theme = this._themes.get(theme);
}
else {
themeId = theme.id;
}
let currTheme = Services.prefs.getCharPref("devtools.theme");
// Change the current theme if it's being dynamically removed together
// with the owner (bootstrapped) extension.
// But, do not change it if the application is just shutting down.
if (!Services.startup.shuttingDown && theme.id == currTheme) {
Services.prefs.setCharPref("devtools.theme", "light");
let data = {
pref: "devtools.theme",
newValue: "light",
oldValue: currTheme
};
gDevTools.emit("pref-changed", data);
this.emit("theme-unregistered", theme);
}
this._themes.delete(themeId);
},
/**
* Get a theme definition if it exists.
*
* @param {string} themeId
* The id of the theme
*
* @return {ThemeDefinition|null} theme
* The ThemeDefinition for the id or null.
*/
getThemeDefinition: function DT_getThemeDefinition(themeId) {
let theme = this._themes.get(themeId);
if (!theme) {
return null;
}
return theme;
},
/**
* Get map of registered themes.
*
* @return {Map} themes
* A map of the the theme definitions registered in this instance
*/
getThemeDefinitionMap: function DT_getThemeDefinitionMap() {
let themes = new Map();
for (let [id, definition] of this._themes) {
if (this.getThemeDefinition(id)) {
themes.set(id, definition);
}
}
return themes;
},
/**
* Get registered themes definitions sorted by ordinal value.
*
* @return {Array} themes
* A sorted array of the theme definitions registered in this instance
*/
getThemeDefinitionArray: function DT_getThemeDefinitionArray() {
let definitions = [];
for (let [id, definition] of this._themes) {
if (this.getThemeDefinition(id)) {
definitions.push(definition);
}
}
return definitions.sort(this.ordinalSort);
},
/**
* Show a Toolbox for a target (either by creating a new one, or if a toolbox
* already exists for the target, by bring to the front the existing one)

Просмотреть файл

@ -5,6 +5,7 @@ support-files =
browser_toolbox_options_disable_js_iframe.html
browser_toolbox_options_disable_cache.sjs
head.js
doc_theme.css
[browser_devtools_api.js]
[browser_dynamic_tool_enabling.js]
@ -32,6 +33,7 @@ skip-if = e10s # Bug 1030318
[browser_toolbox_window_title_changes.js]
[browser_toolbox_zoom.js]
[browser_toolbox_custom_host.js]
[browser_toolbox_theme_registration.js]
# We want this test to run for mochitest-dt as well, so we include it here:
[../../../base/content/test/general/browser_parsable_css.js]

Просмотреть файл

@ -0,0 +1,113 @@
/* vim: set ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
const CHROME_URL = "chrome://mochitests/content/browser/browser/devtools/framework/test/";
let toolbox;
function test()
{
gBrowser.selectedTab = gBrowser.addTab();
let target = TargetFactory.forTab(gBrowser.selectedTab);
gBrowser.selectedBrowser.addEventListener("load", function onLoad(evt) {
gBrowser.selectedBrowser.removeEventListener(evt.type, onLoad, true);
gDevTools.showToolbox(target).then(testRegister);
}, true);
content.location = "data:text/html,test for dynamically registering and unregistering themes";
}
function testRegister(aToolbox)
{
toolbox = aToolbox
gDevTools.once("theme-registered", themeRegistered);
gDevTools.registerTheme({
id: "test-theme",
label: "Test theme",
stylesheets: [CHROME_URL + "doc_theme.css"],
classList: ["theme-test"],
});
}
function themeRegistered(event, themeId)
{
is(themeId, "test-theme", "theme-registered event handler sent theme id");
ok(gDevTools.getThemeDefinitionMap().has(themeId), "theme added to map");
// Test that new theme appears in the Options panel
let target = TargetFactory.forTab(gBrowser.selectedTab);
gDevTools.showToolbox(target, "options").then(() => {
let panel = toolbox.getCurrentPanel();
let doc = panel.panelWin.frameElement.contentDocument;
let themeOption = doc.querySelector("#devtools-theme-box > radio[value=test-theme]");
ok(themeOption, "new theme exists in the Options panel");
// Apply the new theme.
applyTheme();
});
}
function applyTheme()
{
let panelWin = toolbox.getCurrentPanel().panelWin;
let doc = panelWin.frameElement.contentDocument;
let testThemeOption = doc.querySelector("#devtools-theme-box > radio[value=test-theme]");
let lightThemeOption = doc.querySelector("#devtools-theme-box > radio[value=light]");
let color = panelWin.getComputedStyle(testThemeOption).color;
isnot(color, "rgb(255, 0, 0)", "style unapplied");
// Select test theme.
testThemeOption.click();
let color = panelWin.getComputedStyle(testThemeOption).color;
is(color, "rgb(255, 0, 0)", "style applied");
// Select light theme
lightThemeOption.click();
let color = panelWin.getComputedStyle(testThemeOption).color;
isnot(color, "rgb(255, 0, 0)", "style unapplied");
// Select test theme again.
testThemeOption.click();
// Then unregister the test theme.
testUnregister();
}
function testUnregister()
{
gDevTools.unregisterTheme("test-theme");
ok(!gDevTools.getThemeDefinitionMap().has("test-theme"), "theme removed from map");
let panelWin = toolbox.getCurrentPanel().panelWin;
let doc = panelWin.frameElement.contentDocument;
let themeBox = doc.querySelector("#devtools-theme-box");
// The default light theme must be selected now.
is(themeBox.selectedItem, themeBox.querySelector("[value=light]"),
"theme light must be selected");
// Make sure the tab-attaching process is done before we destroy the toolbox.
let target = TargetFactory.forTab(gBrowser.selectedTab);
let actor = target.activeTab.actor;
target.client.attachTab(actor, (response) => {
cleanup();
});
}
function cleanup()
{
toolbox.destroy().then(function() {
toolbox = null;
gBrowser.removeCurrentTab();
finish();
});
}

Просмотреть файл

@ -0,0 +1,3 @@
.theme-test #devtools-theme-box radio {
color: red !important;
}

Просмотреть файл

@ -75,6 +75,8 @@ function OptionsPanel(iframeWindow, toolbox) {
this.isReady = false;
this._prefChanged = this._prefChanged.bind(this);
this._themeRegistered = this._themeRegistered.bind(this);
this._themeUnregistered = this._themeUnregistered.bind(this);
this._addListeners();
@ -101,7 +103,9 @@ OptionsPanel.prototype = {
return targetPromise.then(() => {
this.setupToolsList();
this.setupToolbarButtonsList();
this.setupThemeList();
this.populatePreferences();
this.updateDefaultTheme();
this._disableJSClicked = this._disableJSClicked.bind(this);
@ -119,10 +123,14 @@ OptionsPanel.prototype = {
_addListeners: function() {
gDevTools.on("pref-changed", this._prefChanged);
gDevTools.on("theme-registered", this._themeRegistered);
gDevTools.on("theme-unregistered", this._themeUnregistered);
},
_removeListeners: function() {
gDevTools.off("pref-changed", this._prefChanged);
gDevTools.off("theme-registered", this._themeRegistered);
gDevTools.off("theme-unregistered", this._themeUnregistered);
},
_prefChanged: function(event, data) {
@ -132,6 +140,22 @@ OptionsPanel.prototype = {
cbx.checked = cacheDisabled;
}
else if (data.pref === "devtools.theme") {
this.updateCurrentTheme();
}
},
_themeRegistered: function(event, themeId) {
this.setupThemeList();
},
_themeUnregistered: function(event, theme) {
let themeBox = this.panelDoc.getElementById("devtools-theme-box");
let themeOption = themeBox.querySelector("[value=" + theme.id + "]");
if (themeOption) {
themeBox.removeChild(themeOption);
}
},
setupToolbarButtonsList: function() {
@ -229,6 +253,26 @@ OptionsPanel.prototype = {
this.panelWin.focus();
},
setupThemeList: function() {
let themeBox = this.panelDoc.getElementById("devtools-theme-box");
themeBox.textContent = "";
let createThemeOption = theme => {
let radio = this.panelDoc.createElement("radio");
radio.setAttribute("value", theme.id);
radio.setAttribute("label", theme.label);
return radio;
};
// Populating the default theme list
let themes = gDevTools.getThemeDefinitionArray();
for (let theme of themes) {
themeBox.appendChild(createThemeOption(theme));
}
this.updateCurrentTheme();
},
populatePreferences: function() {
let prefCheckboxes = this.panelDoc.querySelectorAll("checkbox[data-pref]");
for (let checkbox of prefCheckboxes) {
@ -258,9 +302,13 @@ OptionsPanel.prototype = {
pref: this.getAttribute("data-pref"),
newValue: this.selectedItem.getAttribute("value")
};
data.oldValue = GetPref(data.pref);
SetPref(data.pref, data.newValue);
gDevTools.emit("pref-changed", data);
if (data.newValue != data.oldValue) {
gDevTools.emit("pref-changed", data);
}
}.bind(radiogroup));
}
let prefMenulists = this.panelDoc.querySelectorAll("menulist[data-pref]");
@ -292,6 +340,25 @@ OptionsPanel.prototype = {
});
},
updateDefaultTheme: function() {
// Make sure a theme is set in case the previous one coming from
// an extension isn't available anymore.
let themeBox = this.panelDoc.getElementById("devtools-theme-box");
if (themeBox.selectedIndex == -1) {
themeBox.selectedItem = themeBox.querySelector("[value=light]");
}
},
updateCurrentTheme: function() {
let currentTheme = GetPref("devtools.theme");
let themeBox = this.panelDoc.getElementById("devtools-theme-box");
let themeOption = themeBox.querySelector("[value=" + currentTheme + "]");
if (themeOption) {
themeBox.selectedItem = themeOption;
}
},
_populateDisableJSCheckbox: function() {
let cbx = this.panelDoc.getElementById("devtools-disable-javascript");
cbx.checked = !this._origJavascriptEnabled;

Просмотреть файл

@ -33,8 +33,6 @@
class="options-groupbox"
data-pref="devtools.theme"
orient="horizontal">
<radio value="light" label="&options.lightTheme.label;"/>
<radio value="dark" label="&options.darkTheme.label;"/>
</radiogroup>
<label>&options.commonPrefs.label;</label>
<vbox id="commonprefs-options" class="options-groupbox">

Просмотреть файл

@ -357,6 +357,31 @@ for (let definition of defaultTools) {
gDevTools.registerTool(definition);
}
Tools.darkTheme = {
id: "dark",
label: l10n("options.darkTheme.label", toolboxStrings),
ordinal: 1,
stylesheets: ["chrome://browser/skin/devtools/dark-theme.css"],
classList: ["theme-dark"],
};
Tools.lightTheme = {
id: "light",
label: l10n("options.lightTheme.label", toolboxStrings),
ordinal: 2,
stylesheets: ["chrome://browser/skin/devtools/light-theme.css"],
classList: ["theme-light"],
};
let defaultThemes = [
Tools.darkTheme,
Tools.lightTheme,
];
for (let definition of defaultThemes) {
gDevTools.registerTheme(definition);
}
var unloadObserver = {
observe: function(subject, topic, data) {
if (subject.wrappedJSObject === require("@loader/unload")) {
@ -364,6 +389,9 @@ var unloadObserver = {
for (let definition of gDevTools.getToolDefinitionArray()) {
gDevTools.unregisterTool(definition.id);
}
for (let definition of gDevTools.getThemeDefinitionArray()) {
gDevTools.unregisterTheme(definition.id);
}
}
}
};

Просмотреть файл

@ -4,6 +4,8 @@
"use strict";
let { utils: Cu, interfaces: Ci } = Components;
addMessageListener("devtools:test:history", function ({ data }) {
content.history[data.direction]();
});
@ -16,3 +18,11 @@ addMessageListener("devtools:test:reload", function ({ data }) {
data = data || {};
content.location.reload(data.forceget);
});
addMessageListener("devtools:test:forceCC", function () {
let DOMWindowUtils = content.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
DOMWindowUtils.cycleCollect();
DOMWindowUtils.garbageCollect();
DOMWindowUtils.garbageCollect();
});

Просмотреть файл

@ -24,24 +24,35 @@
return;
}
if (oldTheme && newTheme != oldTheme) {
StylesheetUtils.removeSheet(
window,
DEVTOOLS_SKIN_URL + oldTheme + "-theme.css",
"author"
);
let oldThemeDef = gDevTools.getThemeDefinition(oldTheme);
let newThemeDef = gDevTools.getThemeDefinition(newTheme);
// Unload all theme stylesheets related to the old theme.
if (oldThemeDef) {
for (let url of oldThemeDef.stylesheets) {
StylesheetUtils.removeSheet(window, url, "author");
}
}
StylesheetUtils.loadSheet(
window,
DEVTOOLS_SKIN_URL + newTheme + "-theme.css",
"author"
);
// Load all stylesheets associated with the new theme.
let newThemeDef = gDevTools.getThemeDefinition(newTheme);
// Floating scrollbars à la osx
// The theme might not be available anymore (e.g. uninstalled)
// Use the default one.
if (!newThemeDef) {
newThemeDef = gDevTools.getThemeDefinition("light");
}
for (let url of newThemeDef.stylesheets) {
StylesheetUtils.loadSheet(window, url, "author");
}
// Floating scroll-bars like in OSX
let hiddenDOMWindow = Cc["@mozilla.org/appshell/appShellService;1"]
.getService(Ci.nsIAppShellService)
.hiddenDOMWindow;
// TODO: extensions might want to customize scrollbar styles too.
if (!hiddenDOMWindow.matchMedia("(-moz-overlay-scrollbars)").matches) {
let scrollbarsUrl = Services.io.newURI(
DEVTOOLS_SKIN_URL + "floating-scrollbars-light.css", null, null);
@ -62,8 +73,26 @@
forceStyle();
}
documentElement.classList.remove("theme-" + oldTheme);
documentElement.classList.add("theme-" + newTheme);
if (oldThemeDef) {
for (let name of oldThemeDef.classList) {
documentElement.classList.remove(name);
}
if (oldThemeDef.onUnapply) {
oldThemeDef.onUnapply(window, newTheme);
}
}
for (let name of newThemeDef.classList) {
documentElement.classList.add(name);
}
if (newThemeDef.onApply) {
newThemeDef.onApply(window, oldTheme);
}
// Final notification for further theme-switching related logic.
gDevTools.emit("theme-switched", window, newTheme, oldTheme);
}
function handlePrefChange(event, data) {

Просмотреть файл

@ -10,18 +10,23 @@ support-files =
doc_connect-toggle.html
doc_connect-param.html
doc_connect-multi-param.html
doc_change-param.html
440hz_sine.ogg
head.js
[browser_audionode-actor-get-set-param.js]
[browser_audionode-actor-get-type.js]
[browser_audionode-actor-get-param-flags.js]
[browser_audionode-actor-get-params-01.js]
[browser_audionode-actor-get-params-02.js]
[browser_audionode-actor-get-param-flags.js]
[browser_audionode-actor-get-set-param.js]
[browser_audionode-actor-get-type.js]
[browser_audionode-actor-is-source.js]
[browser_webaudio-actor-simple.js]
[browser_webaudio-actor-destroy-node.js]
[browser_webaudio-actor-change-params-01.js]
[browser_webaudio-actor-change-params-02.js]
[browser_webaudio-actor-change-params-03.js]
[browser_webaudio-actor-connect-param.js]
[browser_webaudio-actor-destroy-node.js]
[browser_webaudio-actor-simple.js]
[browser_wa_destroy-node-01.js]
@ -48,4 +53,5 @@ support-files =
# [browser_wa_properties-view-edit-02.js]
# Disabled for too many intermittents bug 1010423
[browser_wa_properties-view-params.js]
[browser_wa_properties-view-change-params.js]
[browser_wa_properties-view-params-objects.js]

Просмотреть файл

@ -0,0 +1,46 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Tests that params view correctly updates changed parameters
* when source code updates them, as well as CHANGE_PARAM events.
*/
function spawnTest() {
let [target, debuggee, panel] = yield initWebAudioEditor(CHANGE_PARAM_URL);
let { panelWin } = panel;
let { gFront, $, $$, EVENTS, WebAudioInspectorView } = panelWin;
let gVars = WebAudioInspectorView._propsView;
// Set parameter polling to 20ms for tests
panelWin.PARAM_POLLING_FREQUENCY = 20;
let started = once(gFront, "start-context");
reload(target);
let [actors] = yield Promise.all([
getN(gFront, "create-node", 3),
waitForGraphRendered(panelWin, 3, 0)
]);
let oscId = actors[1].actorID;
click(panelWin, findGraphNode(panelWin, oscId));
yield once(panelWin, EVENTS.UI_INSPECTOR_NODE_SET);
// Yield twice so we get a diff
yield once(panelWin, EVENTS.CHANGE_PARAM);
let [[_, args]] = yield getSpread(panelWin, EVENTS.CHANGE_PARAM);
is(args.actorID, oscId, "EVENTS.CHANGE_PARAM has correct `actorID`");
ok(args.oldValue < args.newValue, "EVENTS.CHANGE_PARAM has correct `newValue` and `oldValue`");
is(args.param, "detune", "EVENTS.CHANGE_PARAM has correct `param`");
let [[_, args]] = yield getSpread(panelWin, EVENTS.CHANGE_PARAM);
checkVariableView(gVars, 0, { "detune": args.newValue }, "`detune` parameter updated.");
let [[_, args]] = yield getSpread(panelWin, EVENTS.CHANGE_PARAM);
checkVariableView(gVars, 0, { "detune": args.newValue }, "`detune` parameter updated.");
yield teardown(panel);
finish();
}

Просмотреть файл

@ -0,0 +1,46 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test WebAudioActor `change-param` events and front.[en|dis]ableChangeParamEvents
*/
function spawnTest () {
let [target, debuggee, front] = yield initBackend(CHANGE_PARAM_URL);
let [_, nodes] = yield Promise.all([
front.setup({ reload: true }),
getN(front, "create-node", 3)
]);
let osc = nodes[1];
let eventCount = 0;
yield front.enableChangeParamEvents(osc, 20);
front.on("change-param", onChangeParam);
yield getN(front, "change-param", 3);
yield front.disableChangeParamEvents();
let currEventCount = eventCount;
// Be flexible here incase we get an extra counter before the listener is turned off
ok(eventCount >= 3, "Calling `enableChangeParamEvents` should allow front to emit `change-param`.");
yield wait(100);
ok((eventCount - currEventCount) <= 2, "Calling `disableChangeParamEvents` should turn off the listener.");
front.off("change-param", onChangeParam);
yield removeTab(target.tab);
finish();
function onChangeParam ({ newValue, oldValue, param, actorID }) {
is(actorID, osc.actorID, "correct `actorID` in `change-param`.");
is(param, "detune", "correct `param` property in `change-param`.");
ok(newValue > oldValue,
"correct `newValue` (" + newValue + ") and `oldValue` (" + oldValue + ") in `change-param`");
eventCount++;
}
}

Просмотреть файл

@ -0,0 +1,36 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test that listening to param change polling does not break when the AudioNode is collected.
*/
function spawnTest () {
let [target, debuggee, front] = yield initBackend(DESTROY_NODES_URL);
let waitUntilDestroyed = getN(front, "destroy-node", 10);
let [_, nodes] = yield Promise.all([
front.setup({ reload: true }),
getN(front, "create-node", 13)
]);
let bufferNode = nodes[6];
yield front.enableChangeParamEvents(bufferNode, 20);
front.on("change-param", onChangeParam);
forceCC();
yield waitUntilDestroyed;
yield wait(50);
front.off("change-param", onChangeParam);
ok(true, "listening to `change-param` on a dead node doesn't throw.");
yield removeTab(target.tab);
finish();
function onChangeParam (args) {
ok(false, "`change-param` should not be emitted on a node that hasn't changed params or is dead.");
}
}

Просмотреть файл

@ -0,0 +1,32 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
/**
* Test WebAudioActor `change-param` events on special types.
*/
function spawnTest () {
let [target, debuggee, front] = yield initBackend(CHANGE_PARAM_URL);
let [_, nodes] = yield Promise.all([
front.setup({ reload: true }),
getN(front, "create-node", 3)
]);
let shaper = nodes[2];
let eventCount = 0;
yield front.enableChangeParamEvents(shaper, 20);
let onChange = once(front, "change-param");
shaper.setParam("curve", null);
let { newValue, oldValue } = yield onChange;
is(oldValue.type, "object", "`oldValue` should be an object.");
is(oldValue.class, "Float32Array", "`oldValue` should be of class Float32Array.");
is(newValue.type, "null", "`newValue` should be null.");
yield removeTab(target.tab);
finish();
}

Просмотреть файл

@ -0,0 +1,25 @@
<!-- Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ -->
<!doctype html>
<html>
<head>
<meta charset="utf-8"/>
<title>Web Audio Editor test page</title>
</head>
<body>
<script type="text/javascript;version=1.8">
"use strict";
let ctx = new AudioContext();
let osc = ctx.createOscillator();
let shaperNode = ctx.createWaveShaper();
let detuneVal = 0;
shaperNode.curve = new Float32Array(65536);
setInterval(() => osc.detune.value = ++detuneVal, 10);
</script>
</body>
</html>

Просмотреть файл

@ -19,7 +19,9 @@ let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.j
let { WebAudioFront } = devtools.require("devtools/server/actors/webaudio");
let TargetFactory = devtools.TargetFactory;
let mm = null;
const FRAME_SCRIPT_UTILS_URL = "chrome://browser/content/devtools/frame-script-utils.js";
const EXAMPLE_URL = "http://example.com/browser/browser/devtools/webaudioeditor/test/";
const SIMPLE_CONTEXT_URL = EXAMPLE_URL + "doc_simple-context.html";
const COMPLEX_CONTEXT_URL = EXAMPLE_URL + "doc_complex-context.html";
@ -30,6 +32,7 @@ const DESTROY_NODES_URL = EXAMPLE_URL + "doc_destroy-nodes.html";
const CONNECT_TOGGLE_URL = EXAMPLE_URL + "doc_connect-toggle.html";
const CONNECT_PARAM_URL = EXAMPLE_URL + "doc_connect-param.html";
const CONNECT_MULTI_PARAM_URL = EXAMPLE_URL + "doc_connect-multi-param.html";
const CHANGE_PARAM_URL = EXAMPLE_URL + "doc_change-param.html";
// All tests are asynchronous.
waitForExplicitFinish();
@ -133,6 +136,8 @@ function initBackend(aUrl) {
yield target.makeRemote();
let front = new WebAudioFront(target.client, target.form);
loadFrameScripts();
return [target, debuggee, front];
});
}
@ -150,6 +155,8 @@ function initWebAudioEditor(aUrl) {
Services.prefs.setBoolPref("devtools.webaudioeditor.enabled", true);
let toolbox = yield gDevTools.showToolbox(target, "webaudioeditor");
let panel = toolbox.getCurrentPanel();
loadFrameScripts();
return [target, debuggee, panel];
});
}
@ -387,9 +394,12 @@ function countGraphObjects (win) {
* Forces cycle collection and GC, used in AudioNode destruction tests.
*/
function forceCC () {
SpecialPowers.DOMWindowUtils.cycleCollect();
SpecialPowers.DOMWindowUtils.garbageCollect();
SpecialPowers.DOMWindowUtils.garbageCollect();
mm.sendAsyncMessage("devtools:test:forceCC");
}
function loadFrameScripts () {
mm = gBrowser.selectedBrowser.messageManager;
mm.loadFrameScript(FRAME_SCRIPT_UTILS_URL, false);
}
/**

Просмотреть файл

@ -20,9 +20,10 @@ const STRINGS_URI = "chrome://browser/locale/devtools/webaudioeditor.properties"
const L10N = new ViewHelpers.L10N(STRINGS_URI);
const Telemetry = require("devtools/shared/telemetry");
const telemetry = new Telemetry();
let { console } = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
let PARAM_POLLING_FREQUENCY = 1000;
// The panel's window global is an EventEmitter firing the following events:
const EVENTS = {
// Fired when the first AudioNode has been created, signifying
@ -173,6 +174,8 @@ let WebAudioEditorController = {
telemetry.toolOpened("webaudioeditor");
this._onTabNavigated = this._onTabNavigated.bind(this);
this._onThemeChange = this._onThemeChange.bind(this);
this._onSelectNode = this._onSelectNode.bind(this);
this._onChangeParam = this._onChangeParam.bind(this);
gTarget.on("will-navigate", this._onTabNavigated);
gTarget.on("navigate", this._onTabNavigated);
gFront.on("start-context", this._onStartContext);
@ -194,12 +197,15 @@ let WebAudioEditorController = {
window.on(EVENTS.DISCONNECT_NODE, this._onUpdatedContext);
window.on(EVENTS.DESTROY_NODE, this._onUpdatedContext);
window.on(EVENTS.CONNECT_PARAM, this._onUpdatedContext);
// Set up a controller for managing parameter changes per audio node
window.on(EVENTS.UI_SELECT_NODE, this._onSelectNode);
},
/**
* Remove events emitted by the current tab target.
*/
destroy: function() {
destroy: Task.async(function* () {
telemetry.toolClosed("webaudioeditor");
gTarget.off("will-navigate", this._onTabNavigated);
gTarget.off("navigate", this._onTabNavigated);
@ -215,8 +221,11 @@ let WebAudioEditorController = {
window.off(EVENTS.DISCONNECT_NODE, this._onUpdatedContext);
window.off(EVENTS.DESTROY_NODE, this._onUpdatedContext);
window.off(EVENTS.CONNECT_PARAM, this._onUpdatedContext);
window.off(EVENTS.UI_SELECT_NODE, this._onSelectNode);
gDevTools.off("pref-changed", this._onThemeChange);
},
yield gFront.disableChangeParamEvents();
}),
/**
* Called when page is reloaded to show the reload notice and waiting
@ -345,9 +354,21 @@ let WebAudioEditorController = {
/**
* Called when a node param is changed.
*/
_onChangeParam: function({ actor, param, value }) {
window.emit(EVENTS.CHANGE_PARAM, getViewNodeByActor(actor), param, value);
}
_onChangeParam: function (args) {
window.emit(EVENTS.CHANGE_PARAM, args);
},
/**
* Called on UI_SELECT_NODE, used to manage
* `change-param` events on that node.
*/
_onSelectNode: function (_, id) {
let node = getViewNodeById(id);
if (node && node.actor) {
gFront.enableChangeParamEvents(node.actor, PARAM_POLLING_FREQUENCY);
}
},
};
/**

Просмотреть файл

@ -377,6 +377,7 @@ let WebAudioInspectorView = {
this._onNodeSelect = this._onNodeSelect.bind(this);
this._onTogglePaneClick = this._onTogglePaneClick.bind(this);
this._onDestroyNode = this._onDestroyNode.bind(this);
this._onChangeParam = this._onChangeParam.bind(this);
this._inspectorPaneToggleButton.addEventListener("mousedown", this._onTogglePaneClick, false);
this._propsView = new VariablesView($("#properties-tabpanel-content"), GENERIC_VARIABLES_VIEW_SETTINGS);
@ -384,6 +385,7 @@ let WebAudioInspectorView = {
window.on(EVENTS.UI_SELECT_NODE, this._onNodeSelect);
window.on(EVENTS.DESTROY_NODE, this._onDestroyNode);
window.on(EVENTS.CHANGE_PARAM, this._onChangeParam);
},
/**
@ -393,6 +395,7 @@ let WebAudioInspectorView = {
this._inspectorPaneToggleButton.removeEventListener("mousedown", this._onTogglePaneClick);
window.off(EVENTS.UI_SELECT_NODE, this._onNodeSelect);
window.off(EVENTS.DESTROY_NODE, this._onDestroyNode);
window.off(EVENTS.CHANGE_PARAM, this._onChangeParam);
this._inspectorPane = null;
this._inspectorPaneToggleButton = null;
@ -612,7 +615,22 @@ let WebAudioInspectorView = {
if (this._currentNode && this._currentNode.id === id) {
this.setCurrentAudioNode(null);
}
}
},
/**
* Called when `CHANGE_PARAM` is fired. We should ensure that this event is
* for the same node that is currently selected. We check the existence
* of each part of the scope to make sure that if this event was fired
* during a VariablesView rebuild, then we just ignore it.
*/
_onChangeParam: function (_, { param, newValue, oldValue, actorID }) {
if (!this._currentNode || this._currentNode.actor.actorID !== actorID) return;
let scope = this._getAudioPropertiesScope();
if (!scope) return;
let property = scope.get(param);
if (!property) return;
property.setGrip(newValue);
},
};
/**

Просмотреть файл

@ -59,7 +59,7 @@
<menuitem command="cmd_newApp" accesskey="&projectMenu_newApp_accesskey;"/>
<menuitem command="cmd_importPackagedApp" accesskey="&projectMenu_importPackagedApp_accesskey;"/>
<menuitem command="cmd_importHostedApp" accesskey="&projectMenu_importHostedApp_accesskey;"/>
<menuitem command="cmd_showProjectPanel" key="key_showProjectPanel" label="&projectMenu_selectApp_label;" accesskey="&projectMenu_selectApp_accessley;"/>
<menuitem command="cmd_showProjectPanel" key="key_showProjectPanel" label="&projectMenu_selectApp_label;" accesskey="&projectMenu_selectApp_accesskey;"/>
<menuseparator/>
<menuitem command="cmd_play" key="key_play" label="&projectMenu_play_label;" accesskey="&projectMenu_play_accesskey;"/>
<menuitem command="cmd_stop" accesskey="&projectMenu_stop_accesskey;"/>

Просмотреть файл

@ -359,7 +359,7 @@ Experiments.Experiments = function (policy=new Experiments.Policy()) {
// crashes. For forensics purposes, keep the last few log
// messages in memory and upload them in case of crash.
this._forensicsLogs = [];
this._forensicsLogs.length = 3;
this._forensicsLogs.length = 10;
this._log = Object.create(log);
this._log.log = (level, string, params) => {
this._forensicsLogs.shift();

Просмотреть файл

@ -114,8 +114,6 @@
- the heading of the radiobox corresponding to the theme of the developer
- tools. -->
<!ENTITY options.selectDevToolsTheme.label "Choose DevTools theme:">
<!ENTITY options.darkTheme.label "Dark theme">
<!ENTITY options.lightTheme.label "Light theme">
<!-- LOCALIZATION NOTE (options.webconsole.label): This is the label for the
- heading of the group of Web Console preferences in the options panel. -->

Просмотреть файл

@ -70,3 +70,11 @@ browserConsoleCmd.commandkey=j
# LOCALIZATION NOTE (pickButton.tooltip)
# This is the tooltip of the pick button in the toolbox toolbar
pickButton.tooltip=Pick an element from the page
# LOCALIZATION NOTE (options.darkTheme.label)
# Used as a label for dark theme
options.darkTheme.label=Dark theme
# LOCALIZATION NOTE (options.lightTheme.label)
# Used as a label for light theme
options.lightTheme.label=Light theme

Просмотреть файл

@ -13,7 +13,7 @@
<!ENTITY projectMenu_importHostedApp_label "Open Hosted App…">
<!ENTITY projectMenu_importHostedApp_accesskey "H">
<!ENTITY projectMenu_selectApp_label "Open App…">
<!ENTITY projectMenu_selectApp_accessley "S">
<!ENTITY projectMenu_selectApp_accesskey "O">
<!ENTITY projectMenu_play_label "Install and Run">
<!ENTITY projectMenu_play_accesskey "I">
<!ENTITY projectMenu_stop_label "Stop App">

Просмотреть файл

@ -40,6 +40,10 @@ error_cantFetchAddonsJSON=Can't fetch the add-on list: %S
addons_stable=stable
addons_unstable=unstable
# LOCALIZATION NOTE (addons_simulator_label): This label is shown as the name of
# a given simulator version in the "Manage Simulators" pane. %1$S: Firefox OS
# version in the simulator, ex. 1.3. %2$S: Simulator stability label, ex.
# "stable" or "unstable".
addons_simulator_label=Firefox OS %1$S Simulator (%2$S)
addons_install_button=install
addons_uninstall_button=uninstall

Просмотреть файл

@ -77,6 +77,10 @@ menu[disabled="true"].subviewbutton > .menu-right {
-moz-image-region: rect(0, 32px, 16px, 16px);
}
menu.subviewbutton > .menu-right:-moz-locale-dir(rtl) {
transform: scaleX(-1);
}
.subviewbutton > .toolbarbutton-icon {
-moz-margin-end: 5px !important;
}

Просмотреть файл

@ -126,6 +126,10 @@ menu[disabled="true"].subviewbutton > .menu-right {
-moz-image-region: rect(0, 32px, 16px, 16px);
}
menu.subviewbutton > .menu-right:-moz-locale-dir(rtl) {
transform: scaleX(-1);
}
%ifdef WINDOWS_AERO
/* Win8 and beyond. */
@media not all and (-moz-os-version: windows-vista) {

Просмотреть файл

@ -198,6 +198,7 @@
#include "mozilla/dom/HTMLInputElement.h"
#include "mozilla/dom/NodeFilterBinding.h"
#include "mozilla/dom/OwningNonNull.h"
#include "mozilla/dom/TabChild.h"
#include "mozilla/dom/UndoManager.h"
#include "mozilla/dom/WebComponentsBinding.h"
#include "nsFrame.h"
@ -10543,8 +10544,9 @@ nsIDocument::ExitFullscreen(nsIDocument* aDoc, bool aRunAsync)
}
// Returns true if the document is a direct child of a cross process parent
// mozbrowser iframe. This is the case when the document has a null parent,
// and its DocShell reports that it is a browser frame.
// mozbrowser iframe or TabParent. This is the case when the document has
// a null parent and its DocShell reports that it is a browser frame, or
// we can get a TabChild from it.
static bool
HasCrossProcessParent(nsIDocument* aDocument)
{
@ -10562,7 +10564,12 @@ HasCrossProcessParent(nsIDocument* aDocument)
if (!docShell) {
return false;
}
return docShell->GetIsBrowserOrApp();
TabChild* tabChild(TabChild::GetFrom(docShell));
if (!tabChild) {
return false;
}
return true;
}
static bool

Просмотреть файл

@ -61,6 +61,7 @@ public class AppConstants {
* If our MAX_SDK_VERSION is lower than ICS, we must not be an ICS device.
* Otherwise, we need a range check.
*/
public static final boolean preJBMR2 = MAX_SDK_VERSION < 18 || (MIN_SDK_VERSION < 18 && Build.VERSION.SDK_INT < 18);
public static final boolean preJB = MAX_SDK_VERSION < 16 || (MIN_SDK_VERSION < 16 && Build.VERSION.SDK_INT < 16);
public static final boolean preICS = MAX_SDK_VERSION < 14 || (MIN_SDK_VERSION < 14 && Build.VERSION.SDK_INT < 14);
public static final boolean preHCMR2 = MAX_SDK_VERSION < 13 || (MIN_SDK_VERSION < 13 && Build.VERSION.SDK_INT < 13);

Просмотреть файл

@ -2526,7 +2526,8 @@ public class BrowserApp extends GeckoApp
// Disable save as PDF for about:home and xul pages.
saveAsPDF.setEnabled(!(isAboutHome(tab) ||
tab.getContentType().equals("application/vnd.mozilla.xul+xml")));
tab.getContentType().equals("application/vnd.mozilla.xul+xml") ||
tab.getContentType().startsWith("video/")));
// Disable find in page for about:home, since it won't work on Java content.
findInPage.setEnabled(!isAboutHome(tab));

Просмотреть файл

@ -27,11 +27,15 @@ import java.util.Locale;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Queue;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import org.json.JSONException;
import org.json.JSONObject;
import org.mozilla.gecko.AppConstants.Versions;
import org.mozilla.gecko.favicons.OnFaviconLoadedListener;
import org.mozilla.gecko.favicons.decoders.FaviconDecoder;
@ -92,12 +96,14 @@ import android.media.MediaScannerConnection.MediaScannerConnectionClient;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.os.MessageQueue;
import android.os.SystemClock;
import android.os.UserManager;
import android.os.Vibrator;
import android.provider.Settings;
import android.telephony.TelephonyManager;
@ -153,9 +159,6 @@ public class GeckoAppShell
* sVibrationMaybePlaying is true. */
private static long sVibrationEndTime;
/* Default value of how fast we should hint the Android sensors. */
private static int sDefaultSensorHint = 100;
private static Sensor gAccelerometerSensor;
private static Sensor gLinearAccelerometerSensor;
private static Sensor gGyroscopeSensor;
@ -680,14 +683,14 @@ public class GeckoAppShell
if(gOrientationSensor == null)
gOrientationSensor = sm.getDefaultSensor(Sensor.TYPE_ORIENTATION);
if (gOrientationSensor != null)
sm.registerListener(gi.getSensorEventListener(), gOrientationSensor, sDefaultSensorHint);
sm.registerListener(gi.getSensorEventListener(), gOrientationSensor, SensorManager.SENSOR_DELAY_GAME);
break;
case GeckoHalDefines.SENSOR_ACCELERATION:
if(gAccelerometerSensor == null)
gAccelerometerSensor = sm.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
if (gAccelerometerSensor != null)
sm.registerListener(gi.getSensorEventListener(), gAccelerometerSensor, sDefaultSensorHint);
sm.registerListener(gi.getSensorEventListener(), gAccelerometerSensor, SensorManager.SENSOR_DELAY_GAME);
break;
case GeckoHalDefines.SENSOR_PROXIMITY:
@ -708,14 +711,14 @@ public class GeckoAppShell
if(gLinearAccelerometerSensor == null)
gLinearAccelerometerSensor = sm.getDefaultSensor(10 /* API Level 9 - TYPE_LINEAR_ACCELERATION */);
if (gLinearAccelerometerSensor != null)
sm.registerListener(gi.getSensorEventListener(), gLinearAccelerometerSensor, sDefaultSensorHint);
sm.registerListener(gi.getSensorEventListener(), gLinearAccelerometerSensor, SensorManager.SENSOR_DELAY_GAME);
break;
case GeckoHalDefines.SENSOR_GYROSCOPE:
if(gGyroscopeSensor == null)
gGyroscopeSensor = sm.getDefaultSensor(Sensor.TYPE_GYROSCOPE);
if (gGyroscopeSensor != null)
sm.registerListener(gi.getSensorEventListener(), gGyroscopeSensor, sDefaultSensorHint);
sm.registerListener(gi.getSensorEventListener(), gGyroscopeSensor, SensorManager.SENSOR_DELAY_GAME);
break;
default:
Log.w(LOGTAG, "Error! Can't enable unknown SENSOR type " + aSensortype);
@ -2528,6 +2531,39 @@ public class GeckoAppShell
return "DIRECT";
}
@WrapElementForJNI
public static boolean isUserRestricted() {
if (Versions.preJBMR2) {
return false;
}
UserManager mgr = (UserManager)getContext().getSystemService(Context.USER_SERVICE);
Bundle restrictions = mgr.getUserRestrictions();
return !restrictions.isEmpty();
}
@WrapElementForJNI
public static String getUserRestrictions() {
if (Versions.preJBMR2) {
return "{}";
}
JSONObject json = new JSONObject();
UserManager mgr = (UserManager)getContext().getSystemService(Context.USER_SERVICE);
Bundle restrictions = mgr.getUserRestrictions();
Set<String> keys = restrictions.keySet();
for (String key : keys) {
try {
json.put(key, restrictions.get(key));
} catch (JSONException e) {
}
}
return json.toString();
}
/* Downloads the uri pointed to by a share intent, and alters the intent to point to the locally stored file.
*/
public static void downloadImageForIntent(final Intent intent) {

Просмотреть файл

@ -746,8 +746,11 @@ class GeckoLayerClient implements LayerView.Listener, PanZoomTarget
}
@WrapElementForJNI(allowMultithread = true)
public void deactivateProgram() {
public void deactivateProgramAndRestoreState(boolean enableScissor,
int scissorX, int scissorY, int scissorW, int scissorH)
{
mLayerRenderer.deactivateDefaultProgram();
mLayerRenderer.restoreState(enableScissor, scissorX, scissorY, scissorW, scissorH);
}
private void geometryChanged(DisplayPortMetrics displayPort) {

Просмотреть файл

@ -249,6 +249,15 @@ public class LayerRenderer implements Tabs.OnTabsChangedListener {
GLES20.glUseProgram(0);
}
void restoreState(boolean enableScissor, int scissorX, int scissorY, int scissorW, int scissorH) {
GLES20.glScissor(scissorX, scissorY, scissorW, scissorH);
if (enableScissor) {
GLES20.glEnable(GLES20.GL_SCISSOR_TEST);
} else {
GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
}
}
public int getMaxTextureSize() {
return mMaxTextureSize;
}
@ -558,7 +567,7 @@ public class LayerRenderer implements Tabs.OnTabsChangedListener {
@JNITarget
public void drawBackground() {
// Any GL state which is changed here must be restored in
// CompositorOGL::RestoreState
// restoreState(...)
GLES20.glDisable(GLES20.GL_SCISSOR_TEST);
@ -577,7 +586,7 @@ public class LayerRenderer implements Tabs.OnTabsChangedListener {
@JNITarget
public void drawForeground() {
// Any GL state which is changed here must be restored in
// CompositorOGL::RestoreState
// restoreState(...)
/* Draw any extra layers that were added (likely plugins) */
if (mExtraLayers.size() > 0) {

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 279 B

После

Ширина:  |  Высота:  |  Размер: 311 B

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 866 B

После

Ширина:  |  Высота:  |  Размер: 921 B

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 594 B

После

Ширина:  |  Высота:  |  Размер: 633 B

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 1.0 KiB

После

Ширина:  |  Высота:  |  Размер: 1.1 KiB

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 221 B

После

Ширина:  |  Высота:  |  Размер: 254 B

Двоичный файл не отображается.

До

Ширина:  |  Высота:  |  Размер: 325 B

После

Ширина:  |  Высота:  |  Размер: 355 B

Просмотреть файл

@ -6,6 +6,7 @@ package org.mozilla.gecko.tests.components;
import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertEquals;
import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertFalse;
import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertNotNull;
import static org.mozilla.gecko.tests.helpers.AssertionHelper.fAssertTrue;
import java.util.List;
@ -28,6 +29,8 @@ import com.jayway.android.robotium.solo.Solo;
* A class representing any interactions that take place on the app menu.
*/
public class AppMenuComponent extends BaseComponent {
private static final long MAX_WAITTIME_FOR_MENU_UPDATE_IN_MS = 1000L;
private Boolean hasLegacyMenu = null;
public enum MenuItem {
@ -112,6 +115,7 @@ public class AppMenuComponent extends BaseComponent {
pressMenuItem(MenuItem.PAGE.getString(mSolo));
final View pageMenuItemView = findAppMenuItemView(pageMenuItem.getString(mSolo));
fAssertNotNull("The page menu item is not null", pageMenuItemView);
fAssertFalse("The page menu item is not enabled", pageMenuItemView.isEnabled());
fAssertEquals("The page menu item is visible", View.VISIBLE, pageMenuItemView.getVisibility());
} else {
@ -141,6 +145,8 @@ public class AppMenuComponent extends BaseComponent {
* This method is dependent on not having two views with equivalent contentDescription / text.
*/
private View findAppMenuItemView(String text) {
mSolo.waitForText(text, 1, MAX_WAITTIME_FOR_MENU_UPDATE_IN_MS);
final List<View> views = mSolo.getViews();
final List<MenuItemActionBar> menuItemActionBarList = RobotiumUtils.filterViews(MenuItemActionBar.class, views);

Просмотреть файл

@ -105,6 +105,7 @@ skip-if = android_version == "10"
[testNativeWindow]
[testOrderedBroadcast]
[testResourceSubstitutions]
[testRestrictedProfiles]
[testSharedPreferences]
[testSimpleDiscovery]
[testUITelemetry]

Просмотреть файл

@ -1,5 +1,7 @@
package org.mozilla.gecko.tests;
import org.json.JSONObject;
import org.mozilla.gecko.Tabs;
import org.mozilla.gecko.tests.components.AppMenuComponent;
import org.mozilla.gecko.tests.helpers.GeckoHelper;
import org.mozilla.gecko.tests.helpers.NavigationHelper;
@ -23,12 +25,34 @@ public class testAppMenuPathways extends UITest {
// Page menu should be disabled in about:home.
mAppMenu.assertMenuItemIsDisabledAndVisible(AppMenuComponent.PageMenuItem.SAVE_AS_PDF);
// Navigate to a page to test save as pdf functionality.
// Generate a mock Content:LocationChange message with video mime-type for the current tab (tabId = 0).
final JSONObject message = new JSONObject();
try {
message.put("contentType", "video/webm");
message.put("baseDomain", "webmfiles.org");
message.put("type", "Content:LocationChange");
message.put("sameDocument", false);
message.put("userSearch", "");
message.put("uri", getAbsoluteIpUrl("/big-buck-bunny_trailer.webm"));
message.put("tabID", 0);
} catch (Exception ex) {
mAsserter.ok(false, "exception in testSaveAsPDFPathway", ex.toString());
}
// Mock video playback with the generated message and Content:LocationChange event.
Tabs.getInstance().handleMessage("Content:LocationChange", message);
// Save as pdf menu is disabled while playing video.
mAppMenu.assertMenuItemIsDisabledAndVisible(AppMenuComponent.PageMenuItem.SAVE_AS_PDF);
// The above mock video playback test changes Java state, but not the associated JS state.
// Navigate to a new page so that the Java state is cleared.
NavigationHelper.enterAndLoadUrl(StringHelper.ROBOCOP_BLANK_PAGE_01_URL);
mToolbar.assertTitle(StringHelper.ROBOCOP_BLANK_PAGE_01_TITLE);
// Test save as pdf functionality.
// The following call doesn't wait for the resulting pdf but checks that no exception are thrown.
// NOTE: save as pdf functionality must be done at the end as it is slow and cause other test operations to fail.
mAppMenu.pressMenuItem(AppMenuComponent.PageMenuItem.SAVE_AS_PDF);
}
}

Просмотреть файл

@ -0,0 +1,9 @@
package org.mozilla.gecko.tests;
public class testRestrictedProfiles extends JavascriptTest {
public testRestrictedProfiles() {
super("testRestrictedProfiles.js");
}
}

Просмотреть файл

@ -0,0 +1,45 @@
// -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
/* 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/. */
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/ctypes.jsm");
Cu.import("resource://gre/modules/JNI.jsm");
add_task(function test_isUserRestricted() {
// Make sure the parental controls service is available
do_check_true("@mozilla.org/parental-controls-service;1" in Cc);
let pc = Cc["@mozilla.org/parental-controls-service;1"].createInstance(Ci.nsIParentalControlsService);
// In an admin profile, like the tests: enabled = false
// In a restricted profile: enabled = true
do_check_false(pc.parentalControlsEnabled);
//run_next_test();
});
/*
// NOTE: JNI.jsm has no way to call a string method yet
add_task(function test_getUserRestrictions() {
// In an admin profile, like the tests: {}
// In a restricted profile: {"no_modify_accounts":true,"no_share_location":true}
let restrictions = "{}";
let jni = null;
try {
jni = new JNI();
let cls = jni.findClass("org/mozilla/gecko/GeckoAppShell");
let method = jni.getStaticMethodID(cls, "getUserRestrictions", "()Ljava/lang/String;");
restrictions = jni.callStaticStringMethod(cls, method);
} finally {
if (jni != null) {
jni.close();
}
}
do_check_eq(restrictions, "{}");
});
*/
run_next_test();

Просмотреть файл

@ -8,6 +8,7 @@
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/SimpleServiceDiscovery.jsm");
function ok(passed, text) {
do_report_result(passed, text, Components.stack.caller, false);
@ -26,14 +27,38 @@ function middle(element) {
return [x, y];
}
// We must register a target and make a "mock" service for the target
var testTarget = {
target: "test:service",
factory: function(service) { /* dummy */ },
types: ["video/mp4", "video/webm"],
extensions: ["mp4", "webm"]
};
add_test(function setup_browser() {
chromeWin = Services.wm.getMostRecentWindow("navigator:browser");
let BrowserApp = chromeWin.BrowserApp;
do_register_cleanup(function cleanup() {
BrowserApp.closeTab(BrowserApp.getTabForBrowser(browser));
SimpleServiceDiscovery.unregisterTarget(testTarget);
});
// We need to register a target or processService will ignore us
SimpleServiceDiscovery.registerTarget(testTarget);
// Create a pretend service
let service = {
location: "http://mochi.test:8888/tests/robocop/simpleservice.xml",
target: "test:service"
};
do_print("Force a detailed ping from a pretend service");
// Poke the service directly to get the discovery to happen
SimpleServiceDiscovery._processService(service);
// Load our test web page with <video> elements
let url = "http://mochi.test:8888/tests/robocop/video_discovery.html";
browser = BrowserApp.addTab(url, { selected: true, parentId: BrowserApp.selectedTab.id }).browser;
browser.addEventListener("load", function startTests(event) {

Просмотреть файл

@ -458,6 +458,10 @@ var CastingApps = {
}
});
if (items.length == 0) {
return;
}
let prompt = new Prompt({
title: Strings.browser.GetStringFromName("casting.prompt")
}).setSingleChoiceItems(items).show(function(data) {

Просмотреть файл

@ -246,8 +246,8 @@ var SimpleServiceDiscovery = {
getSupportedExtensions: function() {
let extensions = [];
this._targets.forEach(function(target) {
extensions = extensions.concat(target.extensions);
this.services.forEach(function(service) {
extensions = extensions.concat(service.extensions);
}, this);
return extensions.filter(function(extension, pos) {
return extensions.indexOf(extension) == pos;
@ -256,8 +256,8 @@ var SimpleServiceDiscovery = {
getSupportedMimeTypes: function() {
let types = [];
this._targets.forEach(function(target) {
types = types.concat(target.types);
this.services.forEach(function(service) {
types = types.concat(service.types);
}, this);
return types.filter(function(type, pos) {
return types.indexOf(type) == pos;

Просмотреть файл

@ -19,6 +19,10 @@ if not CONFIG['MOZ_DISABLE_PARENTAL_CONTROLS']:
UNIFIED_SOURCES += [
'nsParentalControlsServiceCocoa.mm',
]
elif CONFIG['MOZ_WIDGET_TOOLKIT'] == 'android':
UNIFIED_SOURCES += [
'nsParentalControlsServiceAndroid.cpp',
]
else:
SOURCES += [
'nsParentalControlsServiceDefault.cpp',

Просмотреть файл

@ -0,0 +1,67 @@
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode:nil; c-basic-offset: 2 -*- */
/* 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/. */
#include "nsParentalControlsService.h"
#include "AndroidBridge.h"
#include "nsString.h"
#include "nsIFile.h"
NS_IMPL_ISUPPORTS(nsParentalControlsService, nsIParentalControlsService)
nsParentalControlsService::nsParentalControlsService() :
mEnabled(false)
{
if (mozilla::AndroidBridge::HasEnv()) {
mEnabled = mozilla::widget::android::GeckoAppShell::IsUserRestricted();
}
}
nsParentalControlsService::~nsParentalControlsService()
{
}
NS_IMETHODIMP
nsParentalControlsService::GetParentalControlsEnabled(bool *aResult)
{
*aResult = mEnabled;
return NS_OK;
}
NS_IMETHODIMP
nsParentalControlsService::GetBlockFileDownloadsEnabled(bool *aResult)
{
return NS_ERROR_NOT_AVAILABLE;
}
NS_IMETHODIMP
nsParentalControlsService::GetLoggingEnabled(bool *aResult)
{
return NS_ERROR_NOT_AVAILABLE;
}
NS_IMETHODIMP
nsParentalControlsService::Log(int16_t aEntryType,
bool aBlocked,
nsIURI *aSource,
nsIFile *aTarget)
{
return NS_ERROR_NOT_AVAILABLE;
}
NS_IMETHODIMP
nsParentalControlsService::RequestURIOverride(nsIURI *aTarget,
nsIInterfaceRequestor *aWindowContext,
bool *_retval)
{
return NS_ERROR_NOT_AVAILABLE;
}
NS_IMETHODIMP
nsParentalControlsService::RequestURIOverrides(nsIArray *aTargets,
nsIInterfaceRequestor *aWindowContext,
bool *_retval)
{
return NS_ERROR_NOT_AVAILABLE;
}

Просмотреть файл

@ -375,6 +375,48 @@ addMessageListener("NetworkPrioritizer:AdjustPriority", (msg) => {
loadGroup.adjustPriority(msg.data.adjustment);
});
let DOMFullscreenManager = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
Ci.nsISupportsWeakReference]),
init: function() {
Services.obs.addObserver(this, "ask-parent-to-exit-fullscreen", false);
Services.obs.addObserver(this, "ask-parent-to-rollback-fullscreen", false);
addMessageListener("DOMFullscreen:ChildrenMustExit", () => {
let utils = content.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
utils.exitFullscreen();
});
addEventListener("unload", () => {
Services.obs.removeObserver(this, "ask-parent-to-exit-fullscreen");
Services.obs.removeObserver(this, "ask-parent-to-rollback-fullscreen");
});
},
observe: function(aSubject, aTopic, aData) {
// Observer notifications are global, which means that these notifications
// might be coming from elements that are not actually children within this
// windows' content. We should ignore those. This will not be necessary once
// we fix bug 1053413 and stop using observer notifications for this stuff.
if (aSubject.defaultView.top !== content) {
return;
}
switch (aTopic) {
case "ask-parent-to-exit-fullscreen": {
sendAsyncMessage("DOMFullscreen:RequestExit");
break;
}
case "ask-parent-to-rollback-fullscreen": {
sendAsyncMessage("DOMFullscreen:RequestRollback");
break;
}
}
},
};
DOMFullscreenManager.init();
let AutoCompletePopup = {
QueryInterface: XPCOMUtils.generateQI([Ci.nsIAutoCompletePopup]),

Просмотреть файл

@ -209,6 +209,8 @@
this.messageManager.addMessageListener("DocumentInserted", this);
this.messageManager.addMessageListener("FullZoomChange", this);
this.messageManager.addMessageListener("TextZoomChange", this);
this.messageManager.addMessageListener("DOMFullscreen:RequestExit", this);
this.messageManager.addMessageListener("DOMFullscreen:RequestRollback", this);
this.messageManager.loadFrameScript("chrome://global/content/browser-child.js", true);
if (this.hasAttribute("selectpopup")) {
@ -221,9 +223,17 @@
let RemoteController = Components.utils.import(jsm, {}).RemoteController;
this._controller = new RemoteController(this);
this.controllers.appendController(this._controller);
Services.obs.addObserver(this, "ask-children-to-exit-fullscreen", false);
]]>
</constructor>
<destructor>
<![CDATA[
Services.obs.removeObserver(this, "ask-children-to-exit-fullscreen");
]]>
</destructor>
<method name="receiveMessage">
<parameter name="aMessage"/>
<body><![CDATA[
@ -276,6 +286,20 @@
break;
}
case "DOMFullscreen:RequestExit": {
let windowUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
windowUtils.exitFullscreen();
break;
}
case "DOMFullscreen:RequestRollback": {
let windowUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
windowUtils.remoteFrameFullscreenReverted();
break;
}
default:
// Delegate to browser.xml.
return this._receiveMessage(aMessage);
@ -284,6 +308,19 @@
]]></body>
</method>
<method name="observe">
<parameter name="aSubject"/>
<parameter name="aTopic"/>
<parameter name="aData"/>
<body><![CDATA[
if (aTopic == "ask-children-to-exit-fullscreen") {
if (aSubject == window.document) {
this.messageManager.sendAsyncMessage("DOMFullscreen:ChildrenMustExit");
}
}
]]></body>
</method>
<!--
For out-of-process code, event.screen[XY] is relative to the
left/top of the content view. For in-process code,

Просмотреть файл

@ -7,13 +7,31 @@
const { Cc, Ci, Cu } = require("chrome");
const Services = require("Services");
const gcli = require("gcli/index");
const { DebuggerServer } = require("resource://gre/modules/devtools/dbg-server.jsm");
const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DevToolsLoader",
"resource://gre/modules/devtools/Loader.jsm");
const BRAND_SHORT_NAME = Cc["@mozilla.org/intl/stringbundle;1"]
.getService(Ci.nsIStringBundleService)
.createBundle("chrome://branding/locale/brand.properties")
.GetStringFromName("brandShortName");
XPCOMUtils.defineLazyGetter(this, "debuggerServer", () => {
// Create a separate loader instance, so that we can be sure to receive
// a separate instance of the DebuggingServer from the rest of the
// devtools. This allows us to safely use the tools against even the
// actors and DebuggingServer itself, especially since we can mark
// serverLoader as invisible to the debugger (unlike the usual loader
// settings).
let serverLoader = new DevToolsLoader();
serverLoader.invisibleToDebugger = true;
serverLoader.main("devtools/server/main");
let debuggerServer = serverLoader.DebuggerServer;
debuggerServer.init();
debuggerServer.addBrowserActors();
return debuggerServer;
});
exports.items = [
{
name: "listen",
@ -30,16 +48,12 @@ exports.items = [
}
],
exec: function(args, context) {
if (!DebuggerServer.initialized) {
DebuggerServer.init();
DebuggerServer.addBrowserActors();
}
var reply = DebuggerServer.openListener(args.port);
var reply = debuggerServer.openListener(args.port);
if (!reply) {
throw new Error(gcli.lookup("listenDisabledOutput"));
}
if (DebuggerServer.initialized) {
if (debuggerServer.initialized) {
return gcli.lookupFormat("listenInitOutput", [ "" + args.port ]);
}

Просмотреть файл

@ -4,16 +4,14 @@
"use strict";
const {Cc, Ci, Cu, Cr} = require("chrome");
const Services = require("Services");
const { Promise: promise } = Cu.import("resource://gre/modules/Promise.jsm", {});
const events = require("sdk/event/core");
const { on: systemOn, off: systemOff } = require("sdk/system/events");
const { setTimeout, clearTimeout } = require("sdk/timers");
const protocol = require("devtools/server/protocol");
const { CallWatcherActor, CallWatcherFront } = require("devtools/server/actors/call-watcher");
const { ThreadActor } = require("devtools/server/actors/script");
const { on, once, off, emit } = events;
const { method, Arg, Option, RetVal } = protocol;
@ -27,6 +25,10 @@ exports.unregister = function(handle) {
handle.removeGlobalActor(WebAudioActor);
};
// In milliseconds, how often should AudioNodes poll to see
// if an AudioParam's value has changed to emit to the client.
const PARAM_POLLING_FREQUENCY = 1000;
const AUDIO_GLOBALS = [
"AudioContext", "AudioNode"
];
@ -146,6 +148,10 @@ let AudioNodeActor = exports.AudioNodeActor = protocol.ActorClass({
}
},
destroy: function(conn) {
protocol.Actor.prototype.destroy.call(this, conn);
},
/**
* Returns the name of the audio type.
* Examples: "OscillatorNode", "MediaElementAudioSourceNode"
@ -187,6 +193,7 @@ let AudioNodeActor = exports.AudioNodeActor = protocol.ActorClass({
node[param].value = value;
else
node[param] = value;
return undefined;
} catch (e) {
return constructError(e);
@ -221,14 +228,7 @@ let AudioNodeActor = exports.AudioNodeActor = protocol.ActorClass({
// AudioBuffer or Float32Array references and the like,
// so this just formats the value to be displayed in the VariablesView,
// without using real grips and managing via actor pools.
let grip;
try {
grip = ThreadActor.prototype.createValueGrip(value);
}
catch (e) {
grip = createObjectGrip(value);
}
return grip;
return createGrip(value);
}, {
request: {
param: Arg(0, "string")
@ -252,16 +252,27 @@ let AudioNodeActor = exports.AudioNodeActor = protocol.ActorClass({
}),
/**
* Get an array of objects each containing a `param` and `value` property,
* corresponding to a property name and current value of the audio node.
* Get an array of objects each containing a `param`, `value` and `flags` property,
* corresponding to a property name and current value of the audio node, and any
* associated flags as defined by NODE_PROPERTIES.
*/
getParams: method(function (param) {
getParams: method(function () {
let props = Object.keys(NODE_PROPERTIES[this.type]);
return props.map(prop =>
({ param: prop, value: this.getParam(prop), flags: this.getParamFlags(prop) }));
}, {
response: { params: RetVal("json") }
})
}),
/**
* Returns a boolean indicating whether or not
* the underlying AudioNode has been collected yet or not.
*
* @return Boolean
*/
isAlive: function () {
return !!this.node.get();
}
});
/**
@ -407,6 +418,7 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
this.tabActor = null;
this._initialized = false;
off(this._callWatcher._contentObserver, "global-destroyed", this._onGlobalDestroyed);
this.disableChangeParamEvents();
this._nativeToActorID = null;
this._callWatcher.eraseRecording();
this._callWatcher.finalize();
@ -415,6 +427,59 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
oneway: true
}),
/**
* Takes an AudioNodeActor and a duration specifying how often
* should the node's parameters be polled to detect changes. Emits
* `change-param` when a change is found.
*
* Currently, only one AudioNodeActor can be listened to at a time.
*
* `wait` is used in tests to specify the poll timer.
*/
enableChangeParamEvents: method(function (nodeActor, wait) {
// For now, only have one node being polled
this.disableChangeParamEvents();
// Ignore if node is dead
if (!nodeActor.isAlive()) {
return;
}
let previous = mapAudioParams(nodeActor);
// Store the ID of the node being polled
this._pollingID = nodeActor.actorID;
this.poller = new Poller(() => {
// If node has been collected, disable param polling
if (!nodeActor.isAlive()) {
this.disableChangeParamEvents();
return;
}
let current = mapAudioParams(nodeActor);
diffAudioParams(previous, current).forEach(changed => {
this._onChangeParam(nodeActor, changed);
});
previous = current;
}).on(wait || PARAM_POLLING_FREQUENCY);
}, {
request: {
node: Arg(0, "audionode"),
wait: Arg(1, "nullable:number"),
},
oneway: true
}),
disableChangeParamEvents: method(function () {
if (this.poller) {
this.poller.off();
}
this._pollingID = null;
}, {
oneway: true
}),
/**
* Events emitted by this actor.
*/
@ -437,12 +502,6 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
dest: Option(0, "audionode"),
param: Option(0, "string")
},
"change-param": {
type: "changeParam",
source: Option(0, "audionode"),
param: Option(0, "string"),
value: Option(0, "string")
},
"create-node": {
type: "createNode",
source: Arg(0, "audionode")
@ -450,6 +509,13 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
"destroy-node": {
type: "destroyNode",
source: Arg(0, "audionode")
},
"change-param": {
type: "changeParam",
param: Option(0, "string"),
newValue: Option(0, "json"),
oldValue: Option(0, "json"),
actorID: Option(0, "string")
}
},
@ -473,7 +539,7 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
/**
* Takes an XrayWrapper node, and attaches the node's `nativeID`
* to the AudioParams as `_parentID`, as well as the the type of param
* as a string on `_paramName`.
* as a string on `_paramName`. Used to tag AudioParams for `connect-param` events.
*/
_instrumentParams: function (node) {
let type = getConstructorName(node);
@ -493,6 +559,14 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
* created), so make a new actor and store that.
*/
_getActorByNativeID: function (nativeID) {
// If the WebAudioActor has already been finalized, the `_nativeToActorID`
// map will already be destroyed -- the lingering destruction events
// seem to only occur in e10s, so add an extra check here to disregard
// these late events
if (!this._nativeToActorID) {
return null;
}
// Ensure we have a Number, rather than a string
// return via notification.
nativeID = ~~nativeID;
@ -544,15 +618,12 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
},
/**
* Called when a parameter changes on an audio node
* Called when an AudioParam that's being listened to changes.
* Takes an AudioNodeActor and an object with `newValue`, `oldValue`, and `param` name.
*/
_onParamChange: function (node, param, value) {
let actor = this._getActorByNativeID(node.id);
emit(this, "param-change", {
source: actor,
param: param,
value: value
});
_onChangeParam: function (actor, changed) {
changed.actorID = actor.actorID;
emit(this, "change-param", changed);
},
/**
@ -563,7 +634,8 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
emit(this, "create-node", actor);
},
/** Called when `webaudio-node-demise` is triggered,
/**
* Called when `webaudio-node-demise` is triggered,
* and emits the associated actor to the front if found.
*/
_onDestroyNode: function ({data}) {
@ -576,6 +648,10 @@ let WebAudioActor = exports.WebAudioActor = protocol.ActorClass({
// notifications for a document that no longer exists,
// the mapping should not be found, so we do not emit an event.
if (actor) {
// Turn off polling for changes if on for this node
if (this._pollingID === actor.actorID) {
this.disableChangeParamEvents();
}
this._nativeToActorID.delete(nativeID);
emit(this, "destroy-node", actor);
}
@ -663,17 +739,109 @@ function getConstructorName (obj) {
}
/**
* Create a grip-like object to pass in renderable information
* to the front-end for things like Float32Arrays, AudioBuffers,
* without tracking them in an actor pool.
* Create a value grip for `value`, or fallback to a grip-like object
* for renderable information for the front-end for things like Float32Arrays,
* AudioBuffers, without tracking them in an actor pool.
*/
function createObjectGrip (value) {
return {
type: "object",
preview: {
kind: "ObjectWithText",
text: ""
},
class: getConstructorName(value)
};
function createGrip (value) {
try {
return ThreadActor.prototype.createValueGrip(value);
}
catch (e) {
return {
type: "object",
preview: {
kind: "ObjectWithText",
text: ""
},
class: getConstructorName(value)
};
}
}
/**
* Takes an AudioNodeActor and maps its current parameter values
* to a hash, where the property is the AudioParam name, and value
* is the current value.
*/
function mapAudioParams (node) {
return node.getParams().reduce(function (obj, p) {
obj[p.param] = p.value;
return obj;
}, {});
}
/**
* Takes an object of previous and current values of audio parameters,
* and compares them. If they differ, emit a `change-param` event.
*
* @param Object prev
* Hash of previous set of AudioParam values.
* @param Object current
* Hash of current set of AudioParam values.
*/
function diffAudioParams (prev, current) {
return Object.keys(current).reduce((changed, param) => {
if (!equalGrips(current[param], prev[param])) {
changed.push({
param: param,
oldValue: prev[param],
newValue: current[param]
});
}
return changed;
}, []);
}
/**
* Compares two grip objects to determine if they're equal or not.
*
* @param Any a
* @param Any a
* @return Boolean
*/
function equalGrips (a, b) {
let aType = typeof a;
let bType = typeof b;
if (aType !== bType) {
return false;
} else if (aType === "object") {
// In this case, we are comparing two objects, like an ArrayBuffer or Float32Array,
// or even just plain "null"s (which grip's will have `type` property "null",
// and we have no way of showing more information than its class, so assume
// these are equal since nothing can be updated with information of value.
if (a.type === b.type) {
return true;
}
// Otherwise return false -- this could be a case of a property going from `null`
// to having an ArrayBuffer or an object, in which case we should update it.
return false;
} else {
return a === b;
}
}
/**
* Poller class -- takes a function, and call be turned on and off
* via methods to execute `fn` on the interval specified during `on`.
*/
function Poller (fn) {
this.fn = fn;
}
Poller.prototype.on = function (wait) {
let poller = this;
poller.timer = setTimeout(poll, wait);
function poll () {
poller.fn();
poller.timer = setTimeout(poll, wait);
}
return this;
};
Poller.prototype.off = function () {
if (this.timer) {
clearTimeout(this.timer);
}
return this;
};

Просмотреть файл

@ -55,6 +55,7 @@ jmethodID GeckoAppShell::jGetScreenDepthWrapper = 0;
jmethodID GeckoAppShell::jGetScreenOrientationWrapper = 0;
jmethodID GeckoAppShell::jGetShowPasswordSetting = 0;
jmethodID GeckoAppShell::jGetSystemColoursWrapper = 0;
jmethodID GeckoAppShell::jGetUserRestrictions = 0;
jmethodID GeckoAppShell::jHandleGeckoMessageWrapper = 0;
jmethodID GeckoAppShell::jHandleUncaughtException = 0;
jmethodID GeckoAppShell::jHideProgressDialog = 0;
@ -62,6 +63,7 @@ jmethodID GeckoAppShell::jInitCameraWrapper = 0;
jmethodID GeckoAppShell::jIsNetworkLinkKnown = 0;
jmethodID GeckoAppShell::jIsNetworkLinkUp = 0;
jmethodID GeckoAppShell::jIsTablet = 0;
jmethodID GeckoAppShell::jIsUserRestricted = 0;
jmethodID GeckoAppShell::jKillAnyZombies = 0;
jmethodID GeckoAppShell::jLoadPluginClass = 0;
jmethodID GeckoAppShell::jLockScreenOrientation = 0;
@ -142,6 +144,7 @@ void GeckoAppShell::InitStubs(JNIEnv *jEnv) {
jGetScreenOrientationWrapper = getStaticMethod("getScreenOrientation", "()S");
jGetShowPasswordSetting = getStaticMethod("getShowPasswordSetting", "()Z");
jGetSystemColoursWrapper = getStaticMethod("getSystemColors", "()[I");
jGetUserRestrictions = getStaticMethod("getUserRestrictions", "()Ljava/lang/String;");
jHandleGeckoMessageWrapper = getStaticMethod("handleGeckoMessage", "(Lorg/mozilla/gecko/util/NativeJSContainer;)V");
jHandleUncaughtException = getStaticMethod("handleUncaughtException", "(Ljava/lang/Thread;Ljava/lang/Throwable;)V");
jHideProgressDialog = getStaticMethod("hideProgressDialog", "()V");
@ -149,6 +152,7 @@ void GeckoAppShell::InitStubs(JNIEnv *jEnv) {
jIsNetworkLinkKnown = getStaticMethod("isNetworkLinkKnown", "()Z");
jIsNetworkLinkUp = getStaticMethod("isNetworkLinkUp", "()Z");
jIsTablet = getStaticMethod("isTablet", "()Z");
jIsUserRestricted = getStaticMethod("isUserRestricted", "()Z");
jKillAnyZombies = getStaticMethod("killAnyZombies", "()V");
jLoadPluginClass = getStaticMethod("loadPluginClass", "(Ljava/lang/String;Ljava/lang/String;)Ljava/lang/Class;");
jLockScreenOrientation = getStaticMethod("lockScreenOrientation", "(I)V");
@ -780,6 +784,19 @@ jintArray GeckoAppShell::GetSystemColoursWrapper() {
return ret;
}
jstring GeckoAppShell::GetUserRestrictions() {
JNIEnv *env = AndroidBridge::GetJNIEnv();
if (env->PushLocalFrame(1) != 0) {
AndroidBridge::HandleUncaughtException(env);
MOZ_CRASH("Exception should have caused crash.");
}
jobject temp = env->CallStaticObjectMethod(mGeckoAppShellClass, jGetUserRestrictions);
AndroidBridge::HandleUncaughtException(env);
jstring ret = static_cast<jstring>(env->PopLocalFrame(temp));
return ret;
}
void GeckoAppShell::HandleGeckoMessageWrapper(jobject a0) {
JNIEnv *env = AndroidBridge::GetJNIEnv();
if (env->PushLocalFrame(1) != 0) {
@ -872,6 +889,19 @@ bool GeckoAppShell::IsTablet() {
return temp;
}
bool GeckoAppShell::IsUserRestricted() {
JNIEnv *env = AndroidBridge::GetJNIEnv();
if (env->PushLocalFrame(0) != 0) {
AndroidBridge::HandleUncaughtException(env);
MOZ_CRASH("Exception should have caused crash.");
}
bool temp = env->CallStaticBooleanMethod(mGeckoAppShellClass, jIsUserRestricted);
AndroidBridge::HandleUncaughtException(env);
env->PopLocalFrame(nullptr);
return temp;
}
void GeckoAppShell::KillAnyZombies() {
JNIEnv *env = AndroidBridge::GetJNIEnv();
if (env->PushLocalFrame(0) != 0) {
@ -1717,7 +1747,7 @@ jclass GeckoLayerClient::mGeckoLayerClientClass = 0;
jmethodID GeckoLayerClient::jActivateProgram = 0;
jmethodID GeckoLayerClient::jContentDocumentChanged = 0;
jmethodID GeckoLayerClient::jCreateFrame = 0;
jmethodID GeckoLayerClient::jDeactivateProgram = 0;
jmethodID GeckoLayerClient::jDeactivateProgramAndRestoreState = 0;
jmethodID GeckoLayerClient::jGetDisplayPort = 0;
jmethodID GeckoLayerClient::jIsContentDocumentDisplayed = 0;
jmethodID GeckoLayerClient::jProgressiveUpdateCallback = 0;
@ -1732,7 +1762,7 @@ void GeckoLayerClient::InitStubs(JNIEnv *jEnv) {
jActivateProgram = getMethod("activateProgram", "()V");
jContentDocumentChanged = getMethod("contentDocumentChanged", "()V");
jCreateFrame = getMethod("createFrame", "()Lorg/mozilla/gecko/gfx/LayerRenderer$Frame;");
jDeactivateProgram = getMethod("deactivateProgram", "()V");
jDeactivateProgramAndRestoreState = getMethod("deactivateProgramAndRestoreState", "(ZIIII)V");
jGetDisplayPort = getMethod("getDisplayPort", "(ZZILorg/mozilla/gecko/gfx/ImmutableViewportMetrics;)Lorg/mozilla/gecko/gfx/DisplayPortMetrics;");
jIsContentDocumentDisplayed = getMethod("isContentDocumentDisplayed", "()Z");
jProgressiveUpdateCallback = getMethod("progressiveUpdateCallback", "(ZFFFFFZ)Lorg/mozilla/gecko/gfx/ProgressiveUpdateData;");
@ -1786,14 +1816,21 @@ jobject GeckoLayerClient::CreateFrame() {
return ret;
}
void GeckoLayerClient::DeactivateProgram() {
void GeckoLayerClient::DeactivateProgramAndRestoreState(bool a0, int32_t a1, int32_t a2, int32_t a3, int32_t a4) {
JNIEnv *env = GetJNIForThread();
if (env->PushLocalFrame(0) != 0) {
AndroidBridge::HandleUncaughtException(env);
MOZ_CRASH("Exception should have caused crash.");
}
env->CallVoidMethod(wrapped_obj, jDeactivateProgram);
jvalue args[5];
args[0].z = a0;
args[1].i = a1;
args[2].i = a2;
args[3].i = a3;
args[4].i = a4;
env->CallVoidMethodA(wrapped_obj, jDeactivateProgramAndRestoreState, args);
AndroidBridge::HandleUncaughtException(env);
env->PopLocalFrame(nullptr);
}

Просмотреть файл

@ -62,6 +62,7 @@ public:
static int16_t GetScreenOrientationWrapper();
static bool GetShowPasswordSetting();
static jintArray GetSystemColoursWrapper();
static jstring GetUserRestrictions();
static void HandleGeckoMessageWrapper(jobject a0);
static void HandleUncaughtException(jobject a0, jthrowable a1);
static void HideProgressDialog();
@ -69,6 +70,7 @@ public:
static bool IsNetworkLinkKnown();
static bool IsNetworkLinkUp();
static bool IsTablet();
static bool IsUserRestricted();
static void KillAnyZombies();
static jclass LoadPluginClass(const nsAString& a0, const nsAString& a1);
static void LockScreenOrientation(int32_t a0);
@ -148,6 +150,7 @@ protected:
static jmethodID jGetScreenOrientationWrapper;
static jmethodID jGetShowPasswordSetting;
static jmethodID jGetSystemColoursWrapper;
static jmethodID jGetUserRestrictions;
static jmethodID jHandleGeckoMessageWrapper;
static jmethodID jHandleUncaughtException;
static jmethodID jHideProgressDialog;
@ -155,6 +158,7 @@ protected:
static jmethodID jIsNetworkLinkKnown;
static jmethodID jIsNetworkLinkUp;
static jmethodID jIsTablet;
static jmethodID jIsUserRestricted;
static jmethodID jKillAnyZombies;
static jmethodID jLoadPluginClass;
static jmethodID jLockScreenOrientation;
@ -313,7 +317,7 @@ public:
void ActivateProgram();
void ContentDocumentChanged();
jobject CreateFrame();
void DeactivateProgram();
void DeactivateProgramAndRestoreState(bool a0, int32_t a1, int32_t a2, int32_t a3, int32_t a4);
jobject GetDisplayPort(bool a0, bool a1, int32_t a2, jobject a3);
bool IsContentDocumentDisplayed();
jobject ProgressiveUpdateCallback(bool a0, jfloat a1, jfloat a2, jfloat a3, jfloat a4, jfloat a5, bool a6);
@ -327,7 +331,7 @@ protected:
static jmethodID jActivateProgram;
static jmethodID jContentDocumentChanged;
static jmethodID jCreateFrame;
static jmethodID jDeactivateProgram;
static jmethodID jDeactivateProgramAndRestoreState;
static jmethodID jGetDisplayPort;
static jmethodID jIsContentDocumentDisplayed;
static jmethodID jProgressiveUpdateCallback;

Просмотреть файл

@ -2273,13 +2273,15 @@ nsWindow::DrawWindowUnderlay(LayerManagerComposite* aManager, nsIntRect aRect)
}
gl::GLContext* gl = static_cast<CompositorOGL*>(aManager->GetCompositor())->gl();
gl::ScopedGLState scopedScissorTestState(gl, LOCAL_GL_SCISSOR_TEST);
gl::ScopedScissorRect scopedScissorRectState(gl);
bool scissorEnabled = gl->fIsEnabled(LOCAL_GL_SCISSOR_TEST);
GLint scissorRect[4];
gl->fGetIntegerv(LOCAL_GL_SCISSOR_BOX, scissorRect);
client->ActivateProgram();
if (!mLayerRendererFrame.BeginDrawing(&jniFrame)) return;
if (!mLayerRendererFrame.DrawBackground(&jniFrame)) return;
client->DeactivateProgram(); // redundant, but in case somebody adds code after this...
client->DeactivateProgramAndRestoreState(scissorEnabled,
scissorRect[0], scissorRect[1], scissorRect[2], scissorRect[3]);
}
void
@ -2300,13 +2302,15 @@ nsWindow::DrawWindowOverlay(LayerManagerComposite* aManager, nsIntRect aRect)
mozilla::widget::android::GeckoLayerClient* client = AndroidBridge::Bridge()->GetLayerClient();
gl::GLContext* gl = static_cast<CompositorOGL*>(aManager->GetCompositor())->gl();
gl::ScopedGLState scopedScissorTestState(gl, LOCAL_GL_SCISSOR_TEST);
gl::ScopedScissorRect scopedScissorRectState(gl);
bool scissorEnabled = gl->fIsEnabled(LOCAL_GL_SCISSOR_TEST);
GLint scissorRect[4];
gl->fGetIntegerv(LOCAL_GL_SCISSOR_BOX, scissorRect);
client->ActivateProgram();
if (!mLayerRendererFrame.DrawForeground(&jniFrame)) return;
if (!mLayerRendererFrame.EndDrawing(&jniFrame)) return;
client->DeactivateProgram();
client->DeactivateProgramAndRestoreState(scissorEnabled,
scissorRect[0], scissorRect[1], scissorRect[2], scissorRect[3]);
mLayerRendererFrame.Dispose(env);
}