author | Ryan VanderMeulen <ryanvm@gmail.com> |
Wed, 20 Aug 2014 16:21:52 -0400 | |
changeset 200667 | e7806c9c83f3edc29787133cfaef14ab3bd635c9 |
parent 200666 | a53cafed66b9ba0e4b49c911ad5c50f79d92c2cf (current diff) |
parent 200656 | 6ab867edb95a0e7957d6c707405b4787a2808600 (diff) |
child 200674 | dac8b4a0bd7c67737e2e670a2e2b0d4d69d53dfc |
child 200737 | 5b6dc6fbc556e6f3133368b8a7df99706ac29229 |
child 200762 | 4c24b0282198db337215c7782a213913e6d4d805 |
push id | 27351 |
push user | kwierso@gmail.com |
push date | Wed, 20 Aug 2014 22:56:16 +0000 |
treeherder | mozilla-central@e7806c9c83f3 [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | merge |
milestone | 34.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
editor/composer/res/text_selection_handle.png | file | annotate | diff | comparison | revisions | |
editor/composer/res/text_selection_handle@1.5.png | file | annotate | diff | comparison | revisions | |
editor/composer/res/text_selection_handle@2.png | file | annotate | diff | comparison | revisions | |
editor/libeditor/InsertElementTxn.cpp | file | annotate | diff | comparison | revisions | |
editor/libeditor/InsertElementTxn.h | file | annotate | diff | comparison | revisions | |
js/src/jit/CompilerRoot.h | file | annotate | diff | comparison | revisions |
--- a/b2g/installer/package-manifest.in +++ b/b2g/installer/package-manifest.in @@ -291,16 +291,17 @@ @BINPATH@/components/shellservice.xpt @BINPATH@/components/shistory.xpt @BINPATH@/components/spellchecker.xpt @BINPATH@/components/storage.xpt @BINPATH@/components/telemetry.xpt @BINPATH@/components/toolkit_finalizationwitness.xpt @BINPATH@/components/toolkit_formautofill.xpt @BINPATH@/components/toolkit_osfile.xpt +@BINPATH@/components/toolkit_xulstore.xpt @BINPATH@/components/toolkitprofile.xpt #ifdef MOZ_ENABLE_XREMOTE @BINPATH@/components/toolkitremote.xpt #endif @BINPATH@/components/txtsvc.xpt @BINPATH@/components/txmgr.xpt #ifdef MOZ_USE_NATIVE_UCONV @BINPATH@/components/ucnative.xpt @@ -535,16 +536,18 @@ @BINPATH@/components/HealthReportService.js #endif #ifdef MOZ_CAPTIVEDETECT @BINPATH@/components/CaptivePortalDetectComponents.manifest @BINPATH@/components/captivedetect.js #endif @BINPATH@/components/TelemetryStartup.js @BINPATH@/components/TelemetryStartup.manifest +@BINPATH@/components/XULStore.js +@BINPATH@/components/XULStore.manifest @BINPATH@/components/Webapps.js @BINPATH@/components/Webapps.manifest @BINPATH@/components/AppsService.js @BINPATH@/components/AppsService.manifest @BINPATH@/components/Push.js @BINPATH@/components/Push.manifest @BINPATH@/components/PushServiceLauncher.js @@ -687,19 +690,16 @@ @BINPATH@/res/text_caret_tilt_left.png @BINPATH@/res/text_caret_tilt_left@1.5x.png @BINPATH@/res/text_caret_tilt_left@2.25x.png @BINPATH@/res/text_caret_tilt_left@2x.png @BINPATH@/res/text_caret_tilt_right.png @BINPATH@/res/text_caret_tilt_right@1.5x.png @BINPATH@/res/text_caret_tilt_right@2.25x.png @BINPATH@/res/text_caret_tilt_right@2x.png -@BINPATH@/res/text_selection_handle.png -@BINPATH@/res/text_selection_handle@1.5.png -@BINPATH@/res/text_selection_handle@2.png @BINPATH@/res/grabber.gif #ifdef XP_MACOSX @BINPATH@/res/cursors/* #endif @BINPATH@/res/fonts/* @BINPATH@/res/dtd/* @BINPATH@/res/html/* @BINPATH@/res/langGroups.properties
--- a/browser/base/content/browser-fullScreen.js +++ b/browser/base/content/browser-fullScreen.js @@ -4,16 +4,31 @@ # file, You can obtain one at http://mozilla.org/MPL/2.0/. var FullScreen = { _XULNS: "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", get _fullScrToggler() { 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; // We get the fullscreen event _before_ the window transitions into or out of FS mode. if (event && event.type == "fullscreen") enterFS = !enterFS; // Toggle the View:FullScreen command, which controls elements like the @@ -90,38 +105,56 @@ var FullScreen = { exitDomFullScreen : function() { document.mozCancelFullScreen(); }, handleEvent: function (event) { 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(); break; } }, - 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; } let focusManager = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager); if (focusManager.activeWindow != window) { // The top-level window has lost focus since the request to enter // full-screen was made. Cancel full-screen. @@ -131,17 +164,17 @@ var FullScreen = { // Ensure the sidebar is hidden. if (!document.getElementById("sidebar-box").hidden) toggleSidebar(); 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); gBrowser.tabContainer.addEventListener("TabClose", this.exitDomFullScreen); gBrowser.tabContainer.addEventListener("TabSelect", this.exitDomFullScreen); // Add listener to detect when the fullscreen window is re-focused. // If a fullscreen window loses focus, we show a warning when the @@ -173,17 +206,19 @@ var FullScreen = { this._fullScrToggler.removeEventListener("mouseover", this._expandCallback, false); this._fullScrToggler.removeEventListener("dragenter", this._expandCallback, false); this.cancelWarning(); gBrowser.tabContainer.removeEventListener("TabOpen", this.exitDomFullScreen); gBrowser.tabContainer.removeEventListener("TabClose", this.exitDomFullScreen); gBrowser.tabContainer.removeEventListener("TabSelect", this.exitDomFullScreen); if (!this.useLionFullScreen) window.removeEventListener("activate", this); - this.fullscreenDoc = null; + + window.messageManager + .broadcastAsyncMessage("DOMFullscreen:Cleanup"); } }, observe: function(aSubject, aTopic, aData) { if (aData == "browser.fullscreen.autohide") { if (gPrefService.getBoolPref("browser.fullscreen.autohide")) { gBrowser.mPanelContainer.addEventListener("mousemove", @@ -332,17 +367,17 @@ var FullScreen = { this.warningBox = null; }, setFullscreenAllowed: function(isApproved) { // The "remember decision" checkbox is hidden when showing for documents that // 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, "fullscreen", isApproved ? Services.perms.ALLOW_ACTION : Services.perms.DENY_ACTION, Services.perms.EXPIRE_NEVER); else if (isApproved) { // The user has only temporarily approved fullscren for this fullscreen @@ -365,37 +400,39 @@ var FullScreen = { document.addEventListener("mozfullscreenchange", onFullscreenchange); } } if (this.warningBox) this.warningBox.setAttribute("fade-warning-out", "true"); // 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; } catch (e) { } let hostLabel = document.getElementById("full-screen-domain-text"); let rememberCheckbox = document.getElementById("full-screen-remember-decision"); let isApproved = false; if (host) {
--- a/browser/base/content/browser-fullZoom.js +++ b/browser/base/content/browser-fullZoom.js @@ -14,20 +14,16 @@ var FullZoom = { name: "browser.content.full-zoom", // browser.zoom.siteSpecific preference cache _siteSpecificPref: undefined, // browser.zoom.updateBackgroundTabs preference cache updateBackgroundTabs: undefined, - // One of the possible values for the mousewheel.* preferences. - // From EventStateManager.h. - ACTION_ZOOM: 3, - // This maps the browser to monotonically increasing integer // tokens. _browserTokenMap[browser] is increased each time the zoom is // changed in the browser. See _getBrowserToken and _ignorePendingZoomAccesses. _browserTokenMap: new WeakMap(), // Stores initial locations if we receive onLocationChange // events before we're initialized. _initialLocations: new WeakMap(), @@ -44,18 +40,17 @@ var FullZoom = { Ci.nsIContentPrefObserver, Ci.nsISupportsWeakReference, Ci.nsISupports]), //**************************************************************************// // Initialization & Destruction init: function FullZoom_init() { - // Listen for scrollwheel events so we can save scrollwheel-based changes. - window.addEventListener("DOMMouseScroll", this, false); + gBrowser.addEventListener("ZoomChangeUsingMouseWheel", this); // Register ourselves with the service so we know when our pref changes. this._cps2 = Cc["@mozilla.org/content-pref/service;1"]. getService(Ci.nsIContentPrefService2); this._cps2.addObserverForName(this.name, this); this._siteSpecificPref = gPrefService.getBoolPref("browser.zoom.siteSpecific"); @@ -76,79 +71,35 @@ var FullZoom = { // This should be nulled after initialization. this._initialLocations.clear(); this._initialLocations = null; }, destroy: function FullZoom_destroy() { gPrefService.removeObserver("browser.zoom.", this); this._cps2.removeObserverForName(this.name, this); - window.removeEventListener("DOMMouseScroll", this, false); + gBrowser.removeEventListener("ZoomChangeUsingMouseWheel", this); }, //**************************************************************************// // Event Handlers // nsIDOMEventListener handleEvent: function FullZoom_handleEvent(event) { switch (event.type) { - case "DOMMouseScroll": - this._handleMouseScrolled(event); + case "ZoomChangeUsingMouseWheel": + let browser = this._getTargetedBrowser(event); + this._ignorePendingZoomAccesses(browser); + this._applyZoomToPref(browser); break; } }, - _handleMouseScrolled: function FullZoom__handleMouseScrolled(event) { - // Construct the "mousewheel action" pref key corresponding to this event. - // Based on EventStateManager::WheelPrefs::GetBasePrefName(). - var pref = "mousewheel."; - - var pressedModifierCount = event.shiftKey + event.ctrlKey + event.altKey + - event.metaKey + event.getModifierState("OS"); - if (pressedModifierCount != 1) { - pref += "default."; - } else if (event.shiftKey) { - pref += "with_shift."; - } else if (event.ctrlKey) { - pref += "with_control."; - } else if (event.altKey) { - pref += "with_alt."; - } else if (event.metaKey) { - pref += "with_meta."; - } else { - pref += "with_win."; - } - - pref += "action"; - - // Don't do anything if this isn't a "zoom" scroll event. - var isZoomEvent = false; - try { - isZoomEvent = (gPrefService.getIntPref(pref) == this.ACTION_ZOOM); - } catch (e) {} - if (!isZoomEvent) - return; - - // XXX Lazily cache all the possible action prefs so we don't have to get - // them anew from the pref service for every scroll event? We'd have to - // make sure to observe them so we can update the cache when they change. - - // We have to call _applyZoomToPref in a timeout because we handle the - // event before the event state manager has a chance to apply the zoom - // during EventStateManager::PostHandleEvent. - let browser = gBrowser.selectedBrowser; - let token = this._getBrowserToken(browser); - window.setTimeout(function () { - if (token.isCurrent) - this._applyZoomToPref(browser); - }.bind(this), 0); - }, - // nsIObserver observe: function (aSubject, aTopic, aData) { switch (aTopic) { case "nsPref:changed": switch (aData) { case "browser.zoom.siteSpecific": this._siteSpecificPref = @@ -465,16 +416,40 @@ var FullZoom = { // has no properties, so return false. Check for this case by getting a // property, say, docShell. return map.get(browser) === this.token && browser.parentNode; }, }; }, /** + * Returns the browser that the supplied zoom event is associated with. + * @param event The ZoomChangeUsingMouseWheel event. + * @return The associated browser element, if one exists, otherwise null. + */ + _getTargetedBrowser: function FullZoom__getTargetedBrowser(event) { + let target = event.originalTarget; + + // With remote content browsers, the event's target is the browser + // we're looking for. + const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; + if (target instanceof window.XULElement && + target.localName == "browser" && + target.namespaceURI == XUL_NS) + return target; + + // With in-process content browsers, the event's target is the content + // document. + if (target.nodeType == Node.DOCUMENT_NODE) + return gBrowser.getBrowserForDocument(target); + + throw new Error("Unexpected ZoomChangeUsingMouseWheel event source"); + }, + + /** * Increments the zoom change token for the given browser so that pending * async operations know that it may be unsafe to access they zoom when they * finish. * * @param browser Pending accesses in this browser will be ignored. */ _ignorePendingZoomAccesses: function FullZoom__ignorePendingZoomAccesses(browser) { let map = this._browserTokenMap;
--- a/browser/base/content/browser.js +++ b/browser/base/content/browser.js @@ -18,16 +18,18 @@ XPCOMUtils.defineLazyModuleGetter(this, XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm"); XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu", "resource://gre/modules/CharsetMenu.jsm"); 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", "resource:///modules/AboutHome.jsm"); XPCOMUtils.defineLazyServiceGetter(this, "gDNSService", "@mozilla.org/network/dns-service;1", "nsIDNSService"); @@ -1289,27 +1291,17 @@ var gBrowserInit = { gBrowser.mPanelContainer.addEventListener("InstallBrowserTheme", LightWeightThemeWebInstaller, false, true); gBrowser.mPanelContainer.addEventListener("PreviewBrowserTheme", LightWeightThemeWebInstaller, false, true); gBrowser.mPanelContainer.addEventListener("ResetBrowserThemePreview", LightWeightThemeWebInstaller, false, true); 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 gSyncUI.init(); gFxAccounts.init(); #endif #ifdef MOZ_DATA_REPORTING @@ -1430,17 +1422,17 @@ var gBrowserInit = { // uninit methods don't depend on the services having been initialized). CombinedStopReload.uninit(); gGestureSupport.init(false); gHistorySwipeAnimation.uninit(); - FullScreen.cleanup(); + FullScreen.uninit(); #ifdef MOZ_SERVICES_SYNC gFxAccounts.uninit(); #endif Services.obs.removeObserver(gPluginHandler.pluginCrashed, "plugin-crashed"); try { @@ -2757,24 +2749,16 @@ function SwitchToMetro() { let intervalID = window.setInterval(this._checkDefaultAndSwitchToMetro, 1000); window.setTimeout(function() { window.clearInterval(intervalID); }, 10000); #endif #endif #endif } -function onFullScreen(event) { - FullScreen.toggle(event); -} - -function onMozEnteredDomFullscreen(event) { - FullScreen.enterDomFullscreen(event); -} - function getWebNavigation() { return gBrowser.webNavigation; } function BrowserReloadWithFlags(reloadFlags) { let url = gBrowser.currentURI.spec; if (gBrowser.updateBrowserRemotenessByURL(gBrowser.selectedBrowser, url)) { @@ -3104,17 +3088,17 @@ const BrowserSearch = { } #endif let openSearchPageIfFieldIsNotActive = function(aSearchBar) { if (!aSearchBar || document.activeElement != aSearchBar.textbox.inputField) { let url = gBrowser.currentURI.spec.toLowerCase(); 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"); } } }; let searchBar = this.searchBar;
--- a/browser/base/content/content.js +++ b/browser/base/content/content.js @@ -573,8 +573,45 @@ let PageStyleHandler = { PageStyleHandler.init(); // Keep a reference to the translation content handler to avoid it it being GC'ed. let trHandler = null; 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(); \ No newline at end of file
--- a/browser/base/content/tabbrowser.xml +++ b/browser/base/content/tabbrowser.xml @@ -1220,16 +1220,25 @@ return; } // Otherwise, focus the content area. If we're not using remote tabs, we // can focus the content area right away, since tab switching is synchronous. // 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; if (!gMultiProcessBrowser) { let newFocusedElement = fm.getFocusedElementForWindow(window.content, true, {}); // for anchors, use FLAG_SHOWRING so that it is clear what link was // last clicked when switching back to that tab
--- a/browser/base/content/test/general/browser.ini +++ b/browser/base/content/test/general/browser.ini @@ -271,16 +271,18 @@ skip-if = e10s # Bug 921959 - reload wit [browser_bug880101.js] [browser_bug882977.js] [browser_bug902156.js] skip-if = e10s # Bug 921959 - reload with LOAD_FLAGS_ALLOW_MIXED_CONTENT fails in e10s [browser_bug906190.js] skip-if = buildapp == "mulet" || e10s # Bug ?????? - test directly manipulates content (strange - gets an element from a child which it tries to treat as a string, but that fails) [browser_bug970746.js] skip-if = e10s # Bug ?????? - test directly manipulates content (directly gets elements from the content) +[browser_bug1015721.js] +skip-if = os == 'win' || e10s # Bug 1056146 - FullZoomHelper uses promiseTabLoadEvent() which isn't e10s friendly [browser_canonizeURL.js] skip-if = e10s # Bug ?????? - [JavaScript Error: "Error in AboutHome.sendAboutHomeData TypeError: target.messageManager is undefined" {file: "resource:///modules/AboutHome.jsm" line: 208}] [browser_contentAreaClick.js] [browser_contextSearchTabPosition.js] skip-if = os == "mac" # bug 967013, bug 926729 [browser_ctrlTab.js] skip-if = e10s # Bug ????? - thumbnail captures need e10s love (tabPreviews_capture fails with Argument 1 of CanvasRenderingContext2D.drawWindow does not implement interface Window.) [browser_customize_popupNotification.js]
new file mode 100644 --- /dev/null +++ b/browser/base/content/test/general/browser_bug1015721.js @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this file, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ +"use strict"; + +const TEST_PAGE = "http://example.org/browser/browser/base/content/test/general/zoom_test.html"; + +var gTab1, gTab2, gLevel1; + +function test() { + waitForExplicitFinish(); + + Task.spawn(function () { + gTab1 = gBrowser.addTab(); + gTab2 = gBrowser.addTab(); + + yield FullZoomHelper.selectTabAndWaitForLocationChange(gTab1); + yield FullZoomHelper.load(gTab1, TEST_PAGE); + yield FullZoomHelper.load(gTab2, TEST_PAGE); + }).then(zoomTab1, FullZoomHelper.failAndContinue(finish)); +} + +function dispatchZoomEventToBrowser(browser) { + EventUtils.synthesizeWheel(browser.contentDocument.documentElement, 10, 10, { + ctrlKey: true, deltaY: -1, deltaMode: WheelEvent.DOM_DELTA_LINE + }, browser.contentWindow); +} + +function zoomTab1() { + Task.spawn(function () { + is(gBrowser.selectedTab, gTab1, "Tab 1 is selected"); + FullZoomHelper.zoomTest(gTab1, 1, "Initial zoom of tab 1 should be 1"); + FullZoomHelper.zoomTest(gTab2, 1, "Initial zoom of tab 2 should be 1"); + + let browser1 = gBrowser.getBrowserForTab(gTab1); + dispatchZoomEventToBrowser(browser1); + + gLevel1 = ZoomManager.getZoomForBrowser(browser1); + ok(gLevel1 > 1, "New zoom for tab 1 should be greater than 1"); + + yield FullZoomHelper.selectTabAndWaitForLocationChange(gTab2); + FullZoomHelper.zoomTest(gTab2, gLevel1, "Tab 2 should have zoomed along with tab 1"); + }).then(finishTest, FullZoomHelper.failAndContinue(finish)); +} + +function finishTest() { + Task.spawn(function () { + yield FullZoomHelper.selectTabAndWaitForLocationChange(gTab1); + FullZoom.reset(); + yield FullZoomHelper.removeTabAndWaitForLocationChange(gTab1); + yield FullZoomHelper.selectTabAndWaitForLocationChange(gTab2); + FullZoom.reset(); + yield FullZoomHelper.removeTabAndWaitForLocationChange(gTab2); + }).then(finish, FullZoomHelper.failAndContinue(finish)); +}
--- a/browser/base/content/test/newtab/browser_newtab_search.js +++ b/browser/base/content/test/newtab/browser_newtab_search.js @@ -200,16 +200,34 @@ function runTests() { // Reset changes made to toolbar CustomizableUI.reset(); // Test that Ctrl/Cmd + K will focus the search bar from toolbar. let searchBar = gWindow.document.getElementById("searchbar"); 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); let events = []; for (let engine of gNewEngines) { Services.search.removeEngine(engine); events.push("CurrentState"); @@ -409,8 +427,51 @@ function searchPanel() { function logoImg() { return $("logo"); } 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; +}
--- a/browser/base/content/test/newtab/head.js +++ b/browser/base/content/test/newtab/head.js @@ -9,17 +9,17 @@ Services.prefs.setBoolPref(PREF_NEWTAB_E let tmp = {}; Cu.import("resource://gre/modules/Promise.jsm", tmp); Cu.import("resource://gre/modules/NewTabUtils.jsm", tmp); Cu.import("resource://gre/modules/DirectoryLinksProvider.jsm", tmp); 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); let isMac = ("nsILocalFileMac" in Ci); let isLinux = ("@mozilla.org/gnome-gconf-service;1" in Cc); let isWindows = ("@mozilla.org/windows-registry-key;1" in Cc); let gWindow = window;
--- a/browser/base/content/utilityOverlay.js +++ b/browser/base/content/utilityOverlay.js @@ -304,16 +304,18 @@ function openLinkIn(url, where, params) loadInBackground = false; } } // Raise the target window before loading the URI, since loading it may // 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; if (aAllowThirdPartyFixup) { flags |= Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP; flags |= Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS; } @@ -326,33 +328,40 @@ function openLinkIn(url, where, params) flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_OWNER; w.gBrowser.loadURIWithFlags(url, flags, aReferrerURI, null, aPostData); break; case "tabshifted": 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. // e.g. onclick="checkForMiddleClick(this, event);" function checkForMiddleClick(node, event) { // We should be using the disabled property here instead of the attribute, // but some elements that this function is used with don't support it (e.g. // menuitem).
--- a/browser/components/nsBrowserGlue.js +++ b/browser/components/nsBrowserGlue.js @@ -1315,118 +1315,105 @@ BrowserGlue.prototype = { var notification = notifyBox.appendNotification(text, title, null, notifyBox.PRIORITY_CRITICAL_MEDIUM, buttons); notification.persistence = -1; // Until user closes it }, _migrateUI: function BG__migrateUI() { const UI_VERSION = 23; - const BROWSER_DOCURL = "chrome://browser/content/browser.xul#"; + const BROWSER_DOCURL = "chrome://browser/content/browser.xul"; let currentUIVersion = 0; try { currentUIVersion = Services.prefs.getIntPref("browser.migration.version"); } catch(ex) {} if (currentUIVersion >= UI_VERSION) return; - this._rdf = Cc["@mozilla.org/rdf/rdf-service;1"].getService(Ci.nsIRDFService); - this._dataSource = this._rdf.GetDataSource("rdf:local-store"); - this._dirty = false; + let xulStore = Cc["@mozilla.org/xul/xulstore;1"].getService(Ci.nsIXULStore); if (currentUIVersion < 2) { // This code adds the customizable bookmarks button. - let currentsetResource = this._rdf.GetResource("currentset"); - let toolbarResource = this._rdf.GetResource(BROWSER_DOCURL + "nav-bar"); - let currentset = this._getPersist(toolbarResource, currentsetResource); + let currentset = xulStore.getValue(BROWSER_DOCURL, "nav-bar", "currentset"); // Need to migrate only if toolbar is customized and the element is not found. if (currentset && currentset.indexOf("bookmarks-menu-button-container") == -1) { currentset += ",bookmarks-menu-button-container"; - this._setPersist(toolbarResource, currentsetResource, currentset); + xulStore.setValue(BROWSER_DOCURL, "nav-bar", "currentset", currentset); } } if (currentUIVersion < 3) { // This code merges the reload/stop/go button into the url bar. - let currentsetResource = this._rdf.GetResource("currentset"); - let toolbarResource = this._rdf.GetResource(BROWSER_DOCURL + "nav-bar"); - let currentset = this._getPersist(toolbarResource, currentsetResource); + let currentset = xulStore.getValue(BROWSER_DOCURL, "nav-bar", "currentset"); // Need to migrate only if toolbar is customized and all 3 elements are found. if (currentset && currentset.indexOf("reload-button") != -1 && currentset.indexOf("stop-button") != -1 && currentset.indexOf("urlbar-container") != -1 && currentset.indexOf("urlbar-container,reload-button,stop-button") == -1) { currentset = currentset.replace(/(^|,)reload-button($|,)/, "$1$2") .replace(/(^|,)stop-button($|,)/, "$1$2") .replace(/(^|,)urlbar-container($|,)/, "$1urlbar-container,reload-button,stop-button$2"); - this._setPersist(toolbarResource, currentsetResource, currentset); + xulStore.setValue(BROWSER_DOCURL, "nav-bar", "currentset", currentset); } } if (currentUIVersion < 4) { // This code moves the home button to the immediate left of the bookmarks menu button. - let currentsetResource = this._rdf.GetResource("currentset"); - let toolbarResource = this._rdf.GetResource(BROWSER_DOCURL + "nav-bar"); - let currentset = this._getPersist(toolbarResource, currentsetResource); + let currentset = xulStore.getValue(BROWSER_DOCURL, "nav-bar", "currentset"); // Need to migrate only if toolbar is customized and the elements are found. if (currentset && currentset.indexOf("home-button") != -1 && currentset.indexOf("bookmarks-menu-button-container") != -1) { currentset = currentset.replace(/(^|,)home-button($|,)/, "$1$2") .replace(/(^|,)bookmarks-menu-button-container($|,)/, "$1home-button,bookmarks-menu-button-container$2"); - this._setPersist(toolbarResource, currentsetResource, currentset); + xulStore.setValue(BROWSER_DOCURL, "nav-bar", "currentset", currentset); } } if (currentUIVersion < 5) { // This code uncollapses PersonalToolbar if its collapsed status is not // persisted, and user customized it or changed default bookmarks. - let toolbarResource = this._rdf.GetResource(BROWSER_DOCURL + "PersonalToolbar"); - let collapsedResource = this._rdf.GetResource("collapsed"); - let collapsed = this._getPersist(toolbarResource, collapsedResource); + // // If the user does not have a persisted value for the toolbar's // "collapsed" attribute, try to determine whether it's customized. - if (collapsed === null) { + if (!xulStore.hasValue(BROWSER_DOCURL, "PersonalToolbar", "collapsed")) { // We consider the toolbar customized if it has more than // 3 children, or if it has a persisted currentset value. - let currentsetResource = this._rdf.GetResource("currentset"); - let toolbarIsCustomized = !!this._getPersist(toolbarResource, - currentsetResource); + let toolbarIsCustomized = xulStore.hasValue(BROWSER_DOCURL, + "PersonalToolbar", "currentset"); let getToolbarFolderCount = function () { let toolbarFolder = PlacesUtils.getFolderContents(PlacesUtils.toolbarFolderId).root; let toolbarChildCount = toolbarFolder.childCount; toolbarFolder.containerOpen = false; return toolbarChildCount; }; if (toolbarIsCustomized || getToolbarFolderCount() > 3) { - this._setPersist(toolbarResource, collapsedResource, "false"); + xulStore.setValue(BROWSER_DOCURL, "PersonalToolbar", "collapsed", "false"); } } } if (currentUIVersion < 8) { // Reset homepage pref for users who have it set to google.com/firefox let uri = Services.prefs.getComplexValue("browser.startup.homepage", Ci.nsIPrefLocalizedString).data; if (uri && /^https?:\/\/(www\.)?google(\.\w{2,3}){1,2}\/firefox\/?$/.test(uri)) { Services.prefs.clearUserPref("browser.startup.homepage"); } } if (currentUIVersion < 9) { // This code adds the customizable downloads buttons. - let currentsetResource = this._rdf.GetResource("currentset"); - let toolbarResource = this._rdf.GetResource(BROWSER_DOCURL + "nav-bar"); - let currentset = this._getPersist(toolbarResource, currentsetResource); + let currentset = xulStore.getValue(BROWSER_DOCURL, "nav-bar", "currentset"); // Since the Downloads button is located in the navigation bar by default, // migration needs to happen only if the toolbar was customized using a // previous UI version, and the button was not already placed on the // toolbar manually. if (currentset && currentset.indexOf("downloads-button") == -1) { // The element is added either after the search bar or before the home @@ -1437,17 +1424,17 @@ BrowserGlue.prototype = { "$1search-container,downloads-button$2") } else if (currentset.indexOf("home-button") != -1) { currentset = currentset.replace(/(^|,)home-button($|,)/, "$1downloads-button,home-button$2") } else { currentset = currentset.replace(/(^|,)window-controls($|,)/, "$1downloads-button,window-controls$2") } - this._setPersist(toolbarResource, currentsetResource, currentset); + xulStore.setValue(BROWSER_DOCURL, "nav-bar", "currentset", currentset); } } #ifdef XP_WIN if (currentUIVersion < 10) { // For Windows systems with display set to > 96dpi (i.e. systemDefaultScale // will return a value > 1.0), we want to discard any saved full-zoom settings, // as we'll now be scaling the content according to the system resolution @@ -1467,25 +1454,23 @@ BrowserGlue.prototype = { Services.prefs.clearUserPref("dom.event.contextmenu.enabled"); Services.prefs.clearUserPref("javascript.enabled"); Services.prefs.clearUserPref("permissions.default.image"); } if (currentUIVersion < 12) { // Remove bookmarks-menu-button-container, then place // bookmarks-menu-button into its position. - let currentsetResource = this._rdf.GetResource("currentset"); - let toolbarResource = this._rdf.GetResource(BROWSER_DOCURL + "nav-bar"); - let currentset = this._getPersist(toolbarResource, currentsetResource); + let currentset = xulStore.getValue(BROWSER_DOCURL, "nav-bar", "currentset"); // Need to migrate only if toolbar is customized. if (currentset) { if (currentset.contains("bookmarks-menu-button-container")) { currentset = currentset.replace(/(^|,)bookmarks-menu-button-container($|,)/, "$1bookmarks-menu-button$2"); - this._setPersist(toolbarResource, currentsetResource, currentset); + xulStore.setValue(BROWSER_DOCURL, "nav-bar", "currentset", currentset); } } } if (currentUIVersion < 13) { try { if (Services.prefs.getBoolPref("plugins.hide_infobar_for_missing_plugin")) Services.prefs.setBoolPref("plugins.notifyMissingFlash", false); @@ -1496,62 +1481,54 @@ BrowserGlue.prototype = { if (currentUIVersion < 14) { // DOM Storage doesn't specially handle about: pages anymore. let path = OS.Path.join(OS.Constants.Path.profileDir, "chromeappsstore.sqlite"); OS.File.remove(path); } if (currentUIVersion < 16) { - let toolbarResource = this._rdf.GetResource(BROWSER_DOCURL + "nav-bar"); - let collapsedResource = this._rdf.GetResource("collapsed"); - let isCollapsed = this._getPersist(toolbarResource, collapsedResource); + let isCollapsed = xulStore.getValue(BROWSER_DOCURL, "nav-bar", "collapsed"); if (isCollapsed == "true") { - this._setPersist(toolbarResource, collapsedResource, "false"); + xulStore.setValue(BROWSER_DOCURL, "nav-bar", "collapsed", "false"); } } // Insert the bookmarks-menu-button into the nav-bar if it isn't already // there. if (currentUIVersion < 17) { - let currentsetResource = this._rdf.GetResource("currentset"); - let toolbarResource = this._rdf.GetResource(BROWSER_DOCURL + "nav-bar"); - let currentset = this._getPersist(toolbarResource, currentsetResource); + let currentset = xulStore.getValue(BROWSER_DOCURL, "nav-bar", "currentset"); // Need to migrate only if toolbar is customized. if (currentset) { if (!currentset.contains("bookmarks-menu-button")) { // The button isn't in the nav-bar, so let's look for an appropriate // place to put it. if (currentset.contains("downloads-button")) { currentset = currentset.replace(/(^|,)downloads-button($|,)/, "$1bookmarks-menu-button,downloads-button$2"); } else if (currentset.contains("home-button")) { currentset = currentset.replace(/(^|,)home-button($|,)/, "$1bookmarks-menu-button,home-button$2"); } else { // Just append. currentset = currentset.replace(/(^|,)window-controls($|,)/, "$1bookmarks-menu-button,window-controls$2") } - this._setPersist(toolbarResource, currentsetResource, currentset); + xulStore.setValue(BROWSER_DOCURL, "nav-bar", "currentset", currentset); } } } if (currentUIVersion < 18) { // Remove iconsize and mode from all the toolbars let toolbars = ["navigator-toolbox", "nav-bar", "PersonalToolbar", "addon-bar", "TabsToolbar", "toolbar-menubar"]; for (let resourceName of ["mode", "iconsize"]) { - let resource = this._rdf.GetResource(resourceName); for (let toolbarId of toolbars) { - let toolbar = this._rdf.GetResource(BROWSER_DOCURL + toolbarId); - if (this._getPersist(toolbar, resource)) { - this._setPersist(toolbar, resource); - } + xulStore.removeValue(BROWSER_DOCURL, toolbarId, resourceName); } } } if (currentUIVersion < 19) { let detector = null; try { detector = Services.prefs.getComplexValue("intl.charset.detector", @@ -1564,31 +1541,23 @@ BrowserGlue.prototype = { // If the encoding detector pref value is not reachable from the UI, // reset to default (varies by localization). Services.prefs.clearUserPref("intl.charset.detector"); } } if (currentUIVersion < 20) { // Remove persisted collapsed state from TabsToolbar. - let resource = this._rdf.GetResource("collapsed"); - let toolbar = this._rdf.GetResource(BROWSER_DOCURL + "TabsToolbar"); - if (this._getPersist(toolbar, resource)) { - this._setPersist(toolbar, resource); - } + xulStore.removeValue(BROWSER_DOCURL, "TabsToolbar", "collapsed"); } if (currentUIVersion < 21) { // Make sure the 'toolbarbutton-1' class will always be present from here // on out. - let button = this._rdf.GetResource(BROWSER_DOCURL + "bookmarks-menu-button"); - let classResource = this._rdf.GetResource("class"); - if (this._getPersist(button, classResource)) { - this._setPersist(button, classResource); - } + xulStore.removeValue(BROWSER_DOCURL, "bookmarks-menu-button", "class"); } if (currentUIVersion < 22) { // Reset the Sync promobox count to promote the new FxAccount-based Sync. Services.prefs.clearUserPref("browser.syncPromoViewsLeft"); Services.prefs.clearUserPref("browser.syncPromoViewsLeftMap"); } @@ -1598,59 +1567,20 @@ BrowserGlue.prototype = { try { let name = Services.prefs.getComplexValue(kSelectedEnginePref, Ci.nsIPrefLocalizedString).data; Services.search.currentEngine = Services.search.getEngineByName(name); } catch (ex) {} } } - if (this._dirty) - this._dataSource.QueryInterface(Ci.nsIRDFRemoteDataSource).Flush(); - - delete this._rdf; - delete this._dataSource; - // Update the migration version. Services.prefs.setIntPref("browser.migration.version", UI_VERSION); }, - _getPersist: function BG__getPersist(aSource, aProperty) { - var target = this._dataSource.GetTarget(aSource, aProperty, true); - if (target instanceof Ci.nsIRDFLiteral) - return target.Value; - return null; - }, - - _setPersist: function BG__setPersist(aSource, aProperty, aTarget) { - this._dirty = true; - try { - var oldTarget = this._dataSource.GetTarget(aSource, aProperty, true); - if (oldTarget) { - if (aTarget) - this._dataSource.Change(aSource, aProperty, oldTarget, this._rdf.GetLiteral(aTarget)); - else - this._dataSource.Unassert(aSource, aProperty, oldTarget); - } - else { - this._dataSource.Assert(aSource, aProperty, this._rdf.GetLiteral(aTarget), true); - } - - // Add the entry to the persisted set for this document if it's not there. - // This code is mostly borrowed from XULDocument::Persist. - let docURL = aSource.ValueUTF8.split("#")[0]; - let docResource = this._rdf.GetResource(docURL); - let persistResource = this._rdf.GetResource("http://home.netscape.com/NC-rdf#persist"); - if (!this._dataSource.HasAssertion(docResource, persistResource, aSource, true)) { - this._dataSource.Assert(docResource, persistResource, aSource, true); - } - } - catch(ex) {} - }, - // ------------------------------ // public nsIBrowserGlue members // ------------------------------ sanitize: function BG_sanitize(aParentWindow) { this._sanitizer.sanitize(aParentWindow); },
--- a/browser/components/places/PlacesUIUtils.jsm +++ b/browser/components/places/PlacesUIUtils.jsm @@ -1015,20 +1015,16 @@ this.PlacesUIUtils = { Weave.Service.engineManager.get("tabs").enabled; }, }; XPCOMUtils.defineLazyServiceGetter(PlacesUIUtils, "RDF", "@mozilla.org/rdf/rdf-service;1", "nsIRDFService"); -XPCOMUtils.defineLazyGetter(PlacesUIUtils, "localStore", function() { - return PlacesUIUtils.RDF.GetDataSource("rdf:local-store"); -}); - XPCOMUtils.defineLazyGetter(PlacesUIUtils, "ellipsis", function() { return Services.prefs.getComplexValue("intl.ellipsis", Ci.nsIPrefLocalizedString).data; }); XPCOMUtils.defineLazyGetter(PlacesUIUtils, "useAsyncTransactions", function() { try { return Services.prefs.getBoolPref("browser.places.useAsyncTransactions");
--- a/browser/components/places/content/treeView.js +++ b/browser/components/places/content/treeView.js @@ -18,16 +18,24 @@ function PlacesTreeView(aFlatList, aOnOp this._flatList = aFlatList; this._openContainerCallback = aOnOpenFlatContainer; this._controller = aController; } PlacesTreeView.prototype = { get wrappedJSObject() this, + __xulStore: null, + get _xulStore() { + if (!this.__xulStore) { + this.__xulStore = Cc["@mozilla.org/xul/xulstore;1"].getService(Ci.nsIXULStore); + } + return this.__xulStore; + }, + __dateService: null, get _dateService() { if (!this.__dateService) { this.__dateService = Cc["@mozilla.org/intl/scriptabledateformat;1"]. getService(Ci.nsIScriptableDateFormat); } return this.__dateService; }, @@ -302,21 +310,25 @@ PlacesTreeView.prototype = { this._rows[row] = curChild; rowsInserted++; // Recursively do containers. if (!this._flatList && curChild instanceof Ci.nsINavHistoryContainerResultNode && !this._controller.hasCachedLivemarkInfo(curChild)) { - let resource = this._getResourceForNode(curChild); - let isopen = resource != null && - PlacesUIUtils.localStore.HasAssertion(resource, - openLiteral, - trueLiteral, true); + let uri = curChild.uri; + let isopen = false; + + if (uri) { + let docURI = this._getDocumentURI(); + let val = this._xulStore.getValue(docURI, uri, "open"); + isopen = (val == "true"); + } + if (isopen != curChild.containerOpen) aToOpen.push(curChild); else if (curChild.containerOpen && curChild.childCount > 0) rowsInserted += this._buildVisibleSection(curChild, row + 1, aToOpen); } } return rowsInserted; @@ -1104,21 +1116,26 @@ PlacesTreeView.prototype = { try { return this._getRowForNode(aNode, true); } catch(ex) { } return Ci.nsINavHistoryResultTreeViewer.INDEX_INVISIBLE; }, - _getResourceForNode: function PTV_getResourceForNode(aNode) + // Retrieves an nsIURI for the document + _documentURI: null, + _getDocumentURI: function() { - let uri = aNode.uri; - NS_ASSERT(uri, "if there is no uri, we can't persist the open state"); - return uri ? PlacesUIUtils.RDF.GetResource(uri) : null; + if (!this._documentURI) { + let ioService = Cc["@mozilla.org/network/io-service;1"]. + getService(Ci.nsIIOService); + this._documentURI = ioService.newURI(document.URL, null, null); + } + return this._documentURI; }, // nsITreeView get rowCount() this._rows.length, get selection() this._selection, set selection(val) this._selection = val, getRowProperties: function() { return ""; }, @@ -1492,25 +1509,26 @@ PlacesTreeView.prototype = { let node = this._rows[aRow]; if (this._flatList && this._openContainerCallback) { this._openContainerCallback(node); return; } // Persist containers open status, but never persist livemarks. if (!this._controller.hasCachedLivemarkInfo(node)) { - let resource = this._getResourceForNode(node); - if (resource) { - const openLiteral = PlacesUIUtils.RDF.GetResource("http://home.netscape.com/NC-rdf#open"); - const trueLiteral = PlacesUIUtils.RDF.GetLiteral("true"); + let uri = node.uri; + + if (uri) { + let docURI = this._getDocumentURI(); - if (node.containerOpen) - PlacesUIUtils.localStore.Unassert(resource, openLiteral, trueLiteral); - else - PlacesUIUtils.localStore.Assert(resource, openLiteral, trueLiteral, true); + if (node.containerOpen) { + this._xulStore.removeValue(docURI, uri, "open"); + } else { + this._xulStore.setValue(docURI, uri, "open", "true"); + } } } node.containerOpen = !node.containerOpen; }, cycleHeader: function PTV_cycleHeader(aColumn) { if (!this._result)
--- a/browser/components/places/tests/browser/browser_toolbar_migration.js +++ b/browser/components/places/tests/browser/browser_toolbar_migration.js @@ -1,90 +1,53 @@ /* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ /** * Tests PersonalToolbar migration path. */ - let bg = Cc["@mozilla.org/browser/browserglue;1"].getService(Ci.nsIObserver); let gOriginalMigrationVersion; const BROWSER_URL = getBrowserURL(); let localStore = { - get RDF() Cc["@mozilla.org/rdf/rdf-service;1"].getService(Ci.nsIRDFService), - get store() this.RDF.GetDataSource("rdf:local-store"), + get xulStore() Cc["@mozilla.org/xul/xulstore;1"].getService(Ci.nsIXULStore), - get toolbar() + getValue: function getValue(aProperty) { - delete this.toolbar; - let toolbar = this.RDF.GetResource(BROWSER_URL + "#PersonalToolbar"); - // Add the entry to the persisted set for this document if it's not there. - // See XULDocument::Persist. - let doc = this.RDF.GetResource(BROWSER_URL); - let persist = this.RDF.GetResource("http://home.netscape.com/NC-rdf#persist"); - if (!this.store.HasAssertion(doc, persist, toolbar, true)) { - this.store.Assert(doc, persist, toolbar, true); - } - return this.toolbar = toolbar; + return this.xulStore.getValue(BROWSER_URL, "PersonalToolbar", aProperty); }, - getPersist: function getPersist(aProperty) - { - let property = this.RDF.GetResource(aProperty); - let target = this.store.GetTarget(this.toolbar, property, true); - if (target instanceof Ci.nsIRDFLiteral) - return target.Value; - return null; - }, - - setPersist: function setPersist(aProperty, aValue) + setValue: function setValue(aProperty, aValue) { - let property = this.RDF.GetResource(aProperty); - let value = aValue ? this.RDF.GetLiteral(aValue) : null; - - try { - let oldTarget = this.store.GetTarget(this.toolbar, property, true); - if (oldTarget && value) { - this.store.Change(this.toolbar, property, oldTarget, value); - } - else if (value) { - this.store.Assert(this.toolbar, property, value, true); - } - else if (oldTarget) { - this.store.Unassert(this.toolbar, property, oldTarget); - } - else { - return; - } + if (aValue) { + this.xulStore.setValue(BROWSER_URL, "PersonalToolbar", aProperty, aValue); + } else { + this.xulStore.removeValue(BROWSER_URL, "PersonalToolbar", aProperty); } - catch(ex) { - return; - } - this.store.QueryInterface(Ci.nsIRDFRemoteDataSource).Flush(); } }; let gTests = [ function test_explicitly_collapsed_toolbar() { info("An explicitly collapsed toolbar should not be uncollapsed."); - localStore.setPersist("collapsed", "true"); + localStore.setValue("collapsed", "true"); bg.observe(null, "browser-glue-test", "force-ui-migration"); - is(localStore.getPersist("collapsed"), "true", "Toolbar is collapsed"); + is(localStore.getValue("collapsed"), "true", "Toolbar is collapsed"); }, function test_customized_toolbar() { info("A customized toolbar should be uncollapsed."); - localStore.setPersist("currentset", "splitter"); + localStore.setValue("currentset", "splitter"); bg.observe(null, "browser-glue-test", "force-ui-migration"); - is(localStore.getPersist("collapsed"), "false", "Toolbar has been uncollapsed"); + is(localStore.getValue("collapsed"), "false", "Toolbar has been uncollapsed"); }, function test_many_bookmarks_toolbar() { info("A toolbar with added bookmarks should be uncollapsed."); let ids = []; ids.push( PlacesUtils.bookmarks.insertSeparator(PlacesUtils.toolbarFolderId, @@ -93,43 +56,47 @@ function test_many_bookmarks_toolbar() ids.push( PlacesUtils.bookmarks.insertSeparator(PlacesUtils.toolbarFolderId, PlacesUtils.bookmarks.DEFAULT_INDEX) ); ids.push( PlacesUtils.bookmarks.insertSeparator(PlacesUtils.toolbarFolderId, PlacesUtils.bookmarks.DEFAULT_INDEX) ); + ids.push( + PlacesUtils.bookmarks.insertSeparator(PlacesUtils.toolbarFolderId, + PlacesUtils.bookmarks.DEFAULT_INDEX) + ); bg.observe(null, "browser-glue-test", "force-ui-migration"); - is(localStore.getPersist("collapsed"), "false", "Toolbar has been uncollapsed"); + is(localStore.getValue("collapsed"), "false", "Toolbar has been uncollapsed"); }, ]; function test() { gOriginalMigrationVersion = Services.prefs.getIntPref("browser.migration.version"); registerCleanupFunction(clean); - if (localStore.getPersist("currentset") !== null) { + if (localStore.getValue("currentset") !== null) { info("Toolbar currentset was persisted by a previous test, fixing it."); - localStore.setPersist("currentset", null); + localStore.setValue("currentset", null); } - if (localStore.getPersist("collapsed") !== null) { + if (localStore.getValue("collapsed") !== null) { info("Toolbar collapsed status was persisted by a previous test, fixing it."); - localStore.setPersist("collapsed", null); + localStore.setValue("collapsed", null); } while (gTests.length) { clean(); Services.prefs.setIntPref("browser.migration.version", 4); gTests.shift().call(); } } function clean() { Services.prefs.setIntPref("browser.migration.version", gOriginalMigrationVersion); - localStore.setPersist("currentset", null); - localStore.setPersist("collapsed", null); + localStore.setValue("currentset", null); + localStore.setValue("collapsed", null); }
--- a/browser/components/search/test/browser_426329.js +++ b/browser/components/search/test/browser_426329.js @@ -2,298 +2,304 @@ // we only need ChromeUtils.js for a few files which is why we are using loadSubScript. var ChromeUtils = {}; this._scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"]. getService(Ci.mozIJSSubScriptLoader); this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/ChromeUtils.js", ChromeUtils); 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; +} - const ENGINE_HTML_BASE = "http://mochi.test:8888/browser/browser/components/search/test/test.html"; +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); +} + +// 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); +} - 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 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; +} - searchBar.value = "test"; +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": var engine = ss.getEngineByName("Bug 426329"); ok(engine, "Engine was added."); ss.currentEngine = engine; 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) { + return deferred.promise; +} - 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) { +function* promiseRemoveEngine() { + let deferred = Promise.defer(); + var ss = Services.search; - 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 observer(aSub, aTopic, aData) { + if (aData == "engine-removed") { + Services.obs.removeObserver(observer, "browser-search-engine-modified"); + deferred.resolve(); + } + }; - function testShiftAltReturn() { - init(); - EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true, altKey: true }); - doOnloadOnce(function(event) { + Services.obs.addObserver(observer, "browser-search-engine-modified", false); + var engine = ss.getEngineByName("Bug 426329"); + ss.removeEngine(engine); - 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) { + return deferred.promise; +} - 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) { +var preSelectedBrowser; +var preTabNo; +function* prepareTest() { + preSelectedBrowser = gBrowser.selectedBrowser; + preTabNo = gBrowser.tabs.length; + searchBar = BrowserSearch.searchBar; - 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(); } + let windowFocused = Promise.defer(); + SimpleTest.waitForFocus(windowFocused.resolve, window); + yield windowFocused.promise; - 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(); + let deferred = Promise.defer(); + if (document.activeElement != searchBar) { + searchBar.addEventListener("focus", function onFocus() { + searchBar.removeEventListener("focus", onFocus); + deferred.resolve(); }); - } - - 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(); - }); + searchBar.focus(); + } else { + deferred.resolve(); } - - 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; } +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(); +}); +
--- a/browser/components/search/test/head.js +++ b/browser/components/search/test/head.js @@ -1,11 +1,14 @@ /* 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; let gotActivate = Services.focus.activeWindow == win; function maybeRunCallback() { if (gotLoad && gotActivate) { executeSoon(function() { aCallback(win); }); @@ -83,16 +86,28 @@ function waitForPopupShown(aPopupId, aCa } function removePopupShownListener() { popup.removeEventListener("popupshown", onPopupShown); } popup.addEventListener("popupshown", onPopupShown); 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); } function doOnloadOnce(aCallback) { function doOnloadOnceListener(aEvent) { info("doOnloadOnce: " + aEvent.originalTarget.location); removeDoOnloadOnceListener(); @@ -101,8 +116,21 @@ function doOnloadOnce(aCallback) { }); } function removeDoOnloadOnceListener() { gBrowser.removeEventListener("load", doOnloadOnceListener, true); } 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; +} +
--- a/browser/devtools/framework/gDevTools.jsm +++ b/browser/devtools/framework/gDevTools.jsm @@ -17,23 +17,23 @@ XPCOMUtils.defineLazyModuleGetter(this, XPCOMUtils.defineLazyModuleGetter(this, "console", "resource://gre/modules/devtools/Console.jsm"); 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. this.destroy = this.destroy.bind(this); this._teardown = this._teardown.bind(this); this._testing = false; @@ -225,16 +225,146 @@ DevTools.prototype = { definitions.push(definition); } } 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) * If |toolId| is specified then the displayed toolbox will have the * specified tool selected. * If |hostType| is specified then the toolbox will be displayed using the * specified HostType. * * @param {Target} target
--- a/browser/devtools/framework/test/browser.ini +++ b/browser/devtools/framework/test/browser.ini @@ -1,15 +1,16 @@ [DEFAULT] subsuite = devtools support-files = browser_toolbox_options_disable_js.html 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] [browser_keybindings.js] [browser_new_activation_workflow.js] [browser_target_events.js] [browser_target_remote.js] [browser_toolbox_dynamic_registration.js] @@ -27,12 +28,13 @@ skip-if = e10s # Bug 1030318 [browser_toolbox_sidebar.js] [browser_toolbox_tabsswitch_shortcuts.js] [browser_toolbox_tool_ready.js] [browser_toolbox_window_reload_target.js] [browser_toolbox_window_shortcuts.js] [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]
new file mode 100644 --- /dev/null +++ b/browser/devtools/framework/test/browser_toolbox_theme_registration.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(); + }); +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/framework/test/doc_theme.css @@ -0,0 +1,3 @@ +.theme-test #devtools-theme-box radio { + color: red !important; +}
--- a/browser/devtools/framework/toolbox-options.js +++ b/browser/devtools/framework/toolbox-options.js @@ -70,16 +70,18 @@ function InfallibleGetBoolPref(key) { */ function OptionsPanel(iframeWindow, toolbox) { this.panelDoc = iframeWindow.document; this.panelWin = iframeWindow; this.toolbox = toolbox; this.isReady = false; this._prefChanged = this._prefChanged.bind(this); + this._themeRegistered = this._themeRegistered.bind(this); + this._themeUnregistered = this._themeUnregistered.bind(this); this._addListeners(); const EventEmitter = require("devtools/toolkit/event-emitter"); EventEmitter.decorate(this); } OptionsPanel.prototype = { @@ -96,17 +98,19 @@ OptionsPanel.prototype = { targetPromise = this.target.makeRemote(); } else { targetPromise = promise.resolve(this.target); } return targetPromise.then(() => { this.setupToolsList(); this.setupToolbarButtonsList(); + this.setupThemeList(); this.populatePreferences(); + this.updateDefaultTheme(); this._disableJSClicked = this._disableJSClicked.bind(this); let disableJSNode = this.panelDoc.getElementById("devtools-disable-javascript"); disableJSNode.addEventListener("click", this._disableJSClicked, false); }).then(() => { this.isReady = true; this.emit("ready"); @@ -114,29 +118,49 @@ OptionsPanel.prototype = { }).then(null, function onError(aReason) { Cu.reportError("OptionsPanel open failed. " + aReason.error + ": " + aReason.message); }); }, _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) { if (data.pref === "devtools.cache.disabled") { let cacheDisabled = data.newValue; let cbx = this.panelDoc.getElementById("devtools-disable-cache"); 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() { let enabledToolbarButtonsBox = this.panelDoc.getElementById("enabled-toolbox-buttons-box"); enabledToolbarButtonsBox.textContent = ""; let toggleableButtons = this.toolbox.toolboxButtons; let setToolboxButtonsVisibility = @@ -224,16 +248,36 @@ OptionsPanel.prototype = { if (!atleastOneToolNotSupported) { toolsNotSupportedLabel.style.display = "none"; } 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) { checkbox.checked = GetPref(checkbox.getAttribute("data-pref")); checkbox.addEventListener("command", function() { let data = { pref: this.getAttribute("data-pref"), newValue: this.checked @@ -253,19 +297,23 @@ OptionsPanel.prototype = { break; } } radiogroup.addEventListener("select", function() { let data = { 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]"); for (let menulist of prefMenulists) { let pref = GetPref(menulist.getAttribute("data-pref")); let menuitems = menulist.querySelectorAll("menuitem"); for (let menuitem of menuitems) { let value = menuitem.value; @@ -287,16 +335,35 @@ OptionsPanel.prototype = { this.target.client.attachTab(this.target.activeTab._actor, (response) => { this._origJavascriptEnabled = response.javascriptEnabled; this._populateDisableJSCheckbox(); }); }, + 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; }, /** * Disables JavaScript for the currently loaded tab. We force a page refresh * here because setting docShell.allowJavascript to true fails to block JS
--- a/browser/devtools/framework/toolbox-options.xul +++ b/browser/devtools/framework/toolbox-options.xul @@ -28,18 +28,16 @@ </vbox> <vbox class="options-vertical-pane" flex="1"> <label>&options.selectDevToolsTheme.label;</label> <radiogroup id="devtools-theme-box" 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"> <checkbox label="&options.enablePersistentLogs.label;" tooltiptext="&options.enablePersistentLogs.tooltip;" data-pref="devtools.webconsole.persistlog"/> </vbox> <label>&options.context.inspector;</label>
--- a/browser/devtools/main.js +++ b/browser/devtools/main.js @@ -352,23 +352,51 @@ let defaultTools = [ ]; exports.defaultTools = defaultTools; 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")) { Services.obs.removeObserver(unloadObserver, "sdk:loader:destroy"); for (let definition of gDevTools.getToolDefinitionArray()) { gDevTools.unregisterTool(definition.id); } + for (let definition of gDevTools.getThemeDefinitionArray()) { + gDevTools.unregisterTheme(definition.id); + } } } }; Services.obs.addObserver(unloadObserver, "sdk:loader:destroy", false); events.emit("devtools-loaded", {}); /**
--- a/browser/devtools/shared/frame-script-utils.js +++ b/browser/devtools/shared/frame-script-utils.js @@ -1,18 +1,28 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; +let { utils: Cu, interfaces: Ci } = Components; + addMessageListener("devtools:test:history", function ({ data }) { content.history[data.direction](); }); addMessageListener("devtools:test:navigate", function ({ data }) { content.location = data.location; }); 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(); +});
--- a/browser/devtools/shared/theme-switching.js +++ b/browser/devtools/shared/theme-switching.js @@ -19,34 +19,45 @@ documentElement.style.display = display; // Restore } function switchTheme(newTheme, oldTheme) { if (newTheme === oldTheme) { 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); if (newTheme == "dark") { StylesheetUtils.loadSheet( window, scrollbarsUrl, @@ -57,18 +68,36 @@ window, scrollbarsUrl, "agent" ); } 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) { if (data.pref == "devtools.theme") { switchTheme(data.newValue, data.oldValue); } }
--- a/browser/devtools/webaudioeditor/test/browser.ini +++ b/browser/devtools/webaudioeditor/test/browser.ini @@ -5,28 +5,33 @@ support-files = doc_complex-context.html doc_simple-node-creation.html doc_buffer-and-array.html doc_media-node-creation.html doc_destroy-nodes.html 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-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_webaudio-actor-destroy-node.js] -[browser_webaudio-actor-connect-param.js] [browser_wa_destroy-node-01.js] [browser_wa_first-run.js] [browser_wa_reset-01.js] [browser_wa_reset-02.js] [browser_wa_reset-03.js] @@ -43,9 +48,10 @@ support-files = [browser_wa_inspector-toggle.js] [browser_wa_properties-view.js] [browser_wa_properties-view-media-nodes.js] # [browser_wa_properties-view-edit-01.js] # [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]
new file mode 100644 --- /dev/null +++ b/browser/devtools/webaudioeditor/test/browser_wa_properties-view-change-params.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(); +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/webaudioeditor/test/browser_webaudio-actor-change-params-01.js @@ -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++; + } +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/webaudioeditor/test/browser_webaudio-actor-change-params-02.js @@ -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."); + } +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/webaudioeditor/test/browser_webaudio-actor-change-params-03.js @@ -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(); +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/webaudioeditor/test/doc_change-param.html @@ -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>
--- a/browser/devtools/webaudioeditor/test/head.js +++ b/browser/devtools/webaudioeditor/test/head.js @@ -14,27 +14,30 @@ Services.prefs.setBoolPref("devtools.deb let { Task } = Cu.import("resource://gre/modules/Task.jsm", {}); let { Promise } = Cu.import("resource://gre/modules/Promise.jsm", {}); let { gDevTools } = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); let { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); let { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm", {}); 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"; const SIMPLE_NODES_URL = EXAMPLE_URL + "doc_simple-node-creation.html"; const MEDIA_NODES_URL = EXAMPLE_URL + "doc_media-node-creation.html"; const BUFFER_AND_ARRAY_URL = EXAMPLE_URL + "doc_buffer-and-array.html"; 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(); let gToolEnabled = Services.prefs.getBoolPref("devtools.webaudioeditor.enabled"); registerCleanupFunction(() => { info("finish() was called, cleaning up..."); @@ -128,16 +131,18 @@ function initBackend(aUrl) { return Task.spawn(function*() { let tab = yield addTab(aUrl); let target = TargetFactory.forTab(tab); let debuggee = target.window.wrappedJSObject; yield target.makeRemote(); let front = new WebAudioFront(target.client, target.form); + + loadFrameScripts(); return [target, debuggee, front]; }); } function initWebAudioEditor(aUrl) { info("Initializing a web audio editor pane."); return Task.spawn(function*() { @@ -145,16 +150,18 @@ function initWebAudioEditor(aUrl) { let target = TargetFactory.forTab(tab); let debuggee = target.window.wrappedJSObject; yield target.makeRemote(); Services.prefs.setBoolPref("devtools.webaudioeditor.enabled", true); let toolbox = yield gDevTools.showToolbox(target, "webaudioeditor"); let panel = toolbox.getCurrentPanel(); + + loadFrameScripts(); return [target, debuggee, panel]; }); } function teardown(aPanel) { info("Destroying the web audio editor."); return Promise.all([ @@ -382,19 +389,22 @@ function countGraphObjects (win) { edges: win.document.querySelectorAll(".edgePaths > .edgePath").length } } /** * 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); } /** * List of audio node properties to test against expectations of the AudioNode actor */ const NODE_DEFAULT_VALUES = { "AudioDestinationNode": {},
--- a/browser/devtools/webaudioeditor/webaudioeditor-controller.js +++ b/browser/devtools/webaudioeditor/webaudioeditor-controller.js @@ -15,18 +15,19 @@ const { defer, all } = Cu.import("resour const { Task } = Cu.import("resource://gre/modules/Task.jsm", {}); const require = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}).devtools.require; const EventEmitter = require("devtools/toolkit/event-emitter"); 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 { 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 // that the AudioContext is being used and should be tracked via the editor. START_CONTEXT: "WebAudioEditor:StartContext", // On node creation, connect and disconnect. @@ -168,16 +169,18 @@ function shutdownWebAudioEditor() { let WebAudioEditorController = { /** * Listen for events emitted by the current tab target. */ initialize: function() { 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); gFront.on("create-node", this._onCreateNode); gFront.on("connect-node", this._onConnectNode); gFront.on("connect-param", this._onConnectParam); gFront.on("disconnect-node", this._onDisconnectNode); gFront.on("change-param", this._onChangeParam); @@ -189,39 +192,45 @@ let WebAudioEditorController = { gDevTools.on("pref-changed", this._onThemeChange); // Set up events to refresh the Graph view window.on(EVENTS.CREATE_NODE, this._onUpdatedContext); window.on(EVENTS.CONNECT_NODE, this._onUpdatedContext); 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); gFront.off("start-context", this._onStartContext); gFront.off("create-node", this._onCreateNode); gFront.off("connect-node", this._onConnectNode); gFront.off("connect-param", this._onConnectParam); gFront.off("disconnect-node", this._onDisconnectNode); gFront.off("change-param", this._onChangeParam); gFront.off("destroy-node", this._onDestroyNode); window.off(EVENTS.CREATE_NODE, this._onUpdatedContext); window.off(EVENTS.CONNECT_NODE, this._onUpdatedContext); 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 * for an audio context notice. */ reset: function () { $("#reload-notice").hidden = true; $("#waiting-notice").hidden = false; @@ -340,19 +349,31 @@ let WebAudioEditorController = { let node = getViewNodeByActor(nodeActor); node.disconnect(); window.emit(EVENTS.DISCONNECT_NODE, node.id); }, /** * 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); + } + }, }; /** * Convenient way of emitting events from the panel window. */ EventEmitter.decorate(this); /**
--- a/browser/devtools/webaudioeditor/webaudioeditor-view.js +++ b/browser/devtools/webaudioeditor/webaudioeditor-view.js @@ -372,32 +372,35 @@ let WebAudioInspectorView = { // Hide inspector view on startup this._inspectorPane.setAttribute("width", INSPECTOR_WIDTH); this.toggleInspector({ visible: false, delayed: false, animated: false }); this._onEval = this._onEval.bind(this); 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); this._propsView.eval = this._onEval; window.on(EVENTS.UI_SELECT_NODE, this._onNodeSelect); window.on(EVENTS.DESTROY_NODE, this._onDestroyNode); + window.on(EVENTS.CHANGE_PARAM, this._onChangeParam); }, /** * Destruction function called when the tool cleans up. */ destroy: function () { 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; this._tabsPane = null; }, /** * Toggles the visibility of the AudioNode Inspector. @@ -607,17 +610,32 @@ let WebAudioInspectorView = { /** * Called when `DESTROY_NODE` is fired to remove the node from props view if * it's currently selected. */ _onDestroyNode: function (_, id) { 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); + }, }; /** * Takes an element in an SVG graph and iterates over * ancestors until it finds the graph node container. If not found, * returns null. */
--- a/browser/devtools/webconsole/test/browser.ini +++ b/browser/devtools/webconsole/test/browser.ini @@ -108,17 +108,20 @@ support-files = test-bug_939783_console_trace_duplicates.html test-bug-952277-highlight-nodes-in-vview.html test-bug-609872-cd-iframe-parent.html test-bug-609872-cd-iframe-child.html test-bug-989025-iframe-parent.html test-console-api-stackframe.html test_bug_1010953_cspro.html^headers^ test_bug_1010953_cspro.html + test_bug1045902_console_csp_ignore_reflected_xss_message.html^headers^ + test_bug1045902_console_csp_ignore_reflected_xss_message.html +[browser_bug1045902_console_csp_ignore_reflected_xss_message.js] [browser_bug664688_sandbox_update_after_navigation.js] [browser_bug_638949_copy_link_location.js] [browser_bug_862916_console_dir_and_filter_off.js] [browser_bug_865288_repeat_different_objects.js] [browser_bug_865871_variables_view_close_on_esc_key.js] [browser_bug_869003_inspect_cross_domain_object.js] [browser_bug_871156_ctrlw_close_tab.js] [browser_cached_messages.js]
new file mode 100644 --- /dev/null +++ b/browser/devtools/webconsole/test/browser_bug1045902_console_csp_ignore_reflected_xss_message.js @@ -0,0 +1,53 @@ +/* + * Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ + */ + +/* Description of the test: + * We are loading a file with the following CSP: + * 'reflected-xss filter' + * This directive is not supported, hence we confirm that + * the according message is displayed in the web console. + */ + +const EXPECTED_RESULT = "Not supporting directive 'reflected-xss'. Directive and values will be ignored."; +const TEST_FILE = "http://example.com/browser/browser/devtools/webconsole/test/" + + "test_bug1045902_console_csp_ignore_reflected_xss_message.html"; + +let hud = undefined; + +function test() { + addTab("data:text/html;charset=utf8,Web Console CSP ignoring reflected XSS (bug 1045902)"); + browser.addEventListener("load", function _onLoad() { + browser.removeEventListener("load", _onLoad, true); + openConsole(null, loadDocument); + }, true); +} + +function loadDocument(theHud) { + hud = theHud; + hud.jsterm.clearOutput() + browser.addEventListener("load", onLoad, true); + content.location = TEST_FILE; +} + +function onLoad(aEvent) { + browser.removeEventListener("load", onLoad, true); + testViolationMessage(); +} + +function testViolationMessage() { + let aOutputNode = hud.outputNode; + + waitForSuccess({ + name: "Confirming that CSP logs messages to the console when 'reflected-xss' directive is used!", + validatorFn: function() { + console.log(hud.outputNode.textContent); + let success = false; + success = hud.outputNode.textContent.indexOf(EXPECTED_RESULT) > -1; + return success; + }, + successFn: finishTest, + failureFn: finishTest, + }); +}
new file mode 100644 --- /dev/null +++ b/browser/devtools/webconsole/test/test_bug1045902_console_csp_ignore_reflected_xss_message.html @@ -0,0 +1,10 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="UTF-8"> + <title>Bug 1045902 - CSP: Log console message for 'reflected-xss'</title> +</head> +<body> +Bug 1045902 - CSP: Log console message for 'reflected-xss' +</body> +</html>
new file mode 100644 --- /dev/null +++ b/browser/devtools/webconsole/test/test_bug1045902_console_csp_ignore_reflected_xss_message.html^headers^ @@ -0,0 +1,1 @@ +Content-Security-Policy: reflected-xss filter;
--- a/browser/devtools/webide/content/webide.xul +++ b/browser/devtools/webide/content/webide.xul @@ -54,17 +54,17 @@ </commandset> <menubar id="main-menubar"> <menu id="menu-project" label="&projectMenu_label;" accesskey="&projectMenu_accesskey;"> <menupopup id="menu-project-popup"> <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;"/> <menuitem command="cmd_toggleToolbox" key="key_toggleToolbox" label="&projectMenu_debug_label;" accesskey="&projectMenu_debug_accesskey;"/> <menuseparator/> <menuitem command="cmd_removeProject" accesskey="&projectMenu_remove_accesskey;"/> <menuseparator/> <menuitem command="cmd_showPrefs" label="&projectMenu_showPrefs_label;" accesskey="&projectMenu_showPrefs_accesskey;"/>
--- a/browser/experiments/Experiments.jsm +++ b/browser/experiments/Experiments.jsm @@ -354,17 +354,17 @@ Experiments.Experiments = function (poli let log = Log.repository.getLoggerWithMessagePrefix( "Browser.Experiments.Experiments", "Experiments #" + gExperimentsCounter++ + "::"); // At the time of this writing, Experiments.jsm has severe // 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(); this._forensicsLogs.push(level + ": " + string); log.log(level, string, params); }; this._log.trace("constructor");
--- a/browser/installer/package-manifest.in +++ b/browser/installer/package-manifest.in @@ -298,16 +298,17 @@ #endif @BINPATH@/browser/components/shellservice.xpt @BINPATH@/components/shistory.xpt @BINPATH@/components/spellchecker.xpt @BINPATH@/components/storage.xpt @BINPATH@/components/toolkit_finalizationwitness.xpt @BINPATH@/components/toolkit_formautofill.xpt @BINPATH@/components/toolkit_osfile.xpt +@BINPATH@/components/toolkit_xulstore.xpt @BINPATH@/components/toolkitprofile.xpt #ifdef MOZ_ENABLE_XREMOTE @BINPATH@/components/toolkitremote.xpt #endif @BINPATH@/components/txtsvc.xpt @BINPATH@/components/txmgr.xpt @BINPATH@/components/uconv.xpt @BINPATH@/components/unicharutil.xpt @@ -498,16 +499,18 @@ #ifdef MOZ_CAPTIVEDETECT @BINPATH@/components/CaptivePortalDetectComponents.manifest @BINPATH@/components/captivedetect.js #endif @BINPATH@/components/servicesComponents.manifest @BINPATH@/components/cryptoComponents.manifest @BINPATH@/components/TelemetryStartup.js @BINPATH@/components/TelemetryStartup.manifest +@BINPATH@/components/XULStore.js +@BINPATH@/components/XULStore.manifest @BINPATH@/components/messageWakeupService.js @BINPATH@/components/messageWakeupService.manifest @BINPATH@/components/SettingsManager.js @BINPATH@/components/SettingsManager.manifest @BINPATH@/components/Webapps.js @BINPATH@/components/Webapps.manifest @BINPATH@/components/AppsService.js @BINPATH@/components/AppsService.manifest
--- a/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd +++ b/browser/locales/en-US/chrome/browser/devtools/toolbox.dtd @@ -109,18 +109,16 @@ - the explanation of the * marker on a tool which is currently not supported - for the target of the toolbox. --> <!ENTITY options.toolNotSupported.label "* Not supported for current toolbox target"> <!-- LOCALIZATION NOTE (options.selectDevToolsTheme.label): This is the label for - 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. --> <!ENTITY options.webconsole.label "Web Console"> <!-- LOCALIZATION NOTE (options.timestampMessages.label): This is the - label for the checkbox that toggles timestamps in the Web Console --> <!ENTITY options.timestampMessages.label "Enable timestamps">
--- a/browser/locales/en-US/chrome/browser/devtools/toolbox.properties +++ b/browser/locales/en-US/chrome/browser/devtools/toolbox.properties @@ -65,8 +65,16 @@ scratchpad.keycode=VK_F4 # LOCALIZATION NOTE (browserConsoleCmd.commandkey) # Used for toggling the browser console from the detached toolbox window # Needs to match browserConsoleCmd.commandkey from browser.dtd 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
--- a/browser/locales/en-US/chrome/browser/devtools/webide.dtd +++ b/browser/locales/en-US/chrome/browser/devtools/webide.dtd @@ -8,17 +8,17 @@ <!ENTITY projectMenu_accesskey "P"> <!ENTITY projectMenu_newApp_label "New App…"> <!ENTITY projectMenu_newApp_accesskey "N"> <!ENTITY projectMenu_importPackagedApp_label "Open Packaged App…"> <!ENTITY projectMenu_importPackagedApp_accesskey "P"> <!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"> <!ENTITY projectMenu_stop_accesskey "S"> <!ENTITY projectMenu_debug_label "Debug App"> <!ENTITY projectMenu_debug_accesskey "D"> <!ENTITY projectMenu_remove_label "Remove Project"> <!ENTITY projectMenu_remove_accesskey "R">
--- a/browser/locales/en-US/chrome/browser/devtools/webide.properties +++ b/browser/locales/en-US/chrome/browser/devtools/webide.properties @@ -35,16 +35,20 @@ error_operationFail=Operation failed: %1 # Variable: app name error_cantConnectToApp=Can't connect to app: %1$S # Variable: error message (in english) 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 addons_adb_label=ADB Helper Add-on addons_adb_warning=USB devices won't be detected without this add-on addons_status_unknown=? addons_status_installed=Installed addons_status_uninstalled=Not Installed
--- a/browser/themes/linux/customizableui/panelUIOverlay.css +++ b/browser/themes/linux/customizableui/panelUIOverlay.css @@ -72,16 +72,20 @@ menu.subviewbutton > .menu-right { width: 16px; height: 16px; } 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; } .subviewbutton > .menu-right, .subviewbutton > .menu-iconic-left { padding-top: 1px; /* These need !important to override menu.css */
--- a/browser/themes/windows/customizableui/panelUIOverlay.css +++ b/browser/themes/windows/customizableui/panelUIOverlay.css @@ -121,16 +121,20 @@ menu.subviewbutton > .menu-right { list-style-image: url(chrome://browser/skin/places/bookmarks-menu-arrow.png); -moz-image-region: rect(0, 16px, 16px, 0); } 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) { @media not all and (-moz-os-version: windows-win7) { panelview .toolbarbutton-1, .subviewbutton, .widget-overflow-list .toolbarbutton-1, .panelUI-grid .toolbarbutton-1 > .toolbarbutton-menubutton-button,
--- a/build/sanitizers/lsan_suppressions.txt +++ b/build/sanitizers/lsan_suppressions.txt @@ -56,16 +56,20 @@ leak:fsmdef_ev_createanswer leak:vcmRxAllocICE_s leak:vcmRxStartICE_m # About 50KB of leaks under this stack. leak:ccsnap_EscapeStrToLocaleStr leak:gsmsdp_add_default_audio_formats_to_local_sdp leak:gsmsdp_add_default_video_formats_to_local_sdp leak:CCAPI_CallInfo_getMediaStreams +# Intermittent Mochitest 3 WebRTC leaks, as seen in bug 1055910. +leak:sdp_build_attr_ice_attr +leak:VcmSIPCCBinding::CandidateReady + ### ### Many leaks only affect some test suites. The suite annotations are not checked. ### # Bug 981195 - Small leak in the parser. m4 leak:TypeCompartment::fixObjectType
--- a/content/base/src/nsCSPParser.cpp +++ b/content/base/src/nsCSPParser.cpp @@ -779,16 +779,27 @@ nsCSPParser::directiveName() // Check if it is a valid directive if (!CSP_IsValidDirective(mCurToken)) { const char16_t* params[] = { mCurToken.get() }; logWarningErrorToConsole(nsIScriptError::warningFlag, "couldNotProcessUnknownDirective", params, ArrayLength(params)); return nullptr; } + // The directive 'reflected-xss' is part of CSP 1.1, see: + // http://www.w3.org/TR/2014/WD-CSP11-20140211/#reflected-xss + // Currently we are not supporting that directive, hence we log a + // warning to the console and ignore the directive including its values. + if (CSP_IsDirective(mCurToken, CSP_REFLECTED_XSS)) { + const char16_t* params[] = { mCurToken.get() }; + logWarningErrorToConsole(nsIScriptError::warningFlag, "notSupportingDirective", + params, ArrayLength(params)); + return nullptr; + } + // Make sure the directive does not already exist // (see http://www.w3.org/TR/CSP11/#parsing) if (mPolicy->directiveExists(CSP_DirectiveToEnum(mCurToken))) { const char16_t* params[] = { mCurToken.get() }; logWarningErrorToConsole(nsIScriptError::warningFlag, "duplicateDirective", params, ArrayLength(params)); return nullptr; }
--- a/content/base/src/nsCSPUtils.h +++ b/content/base/src/nsCSPUtils.h @@ -65,33 +65,35 @@ enum CSPDirective { CSP_STYLE_SRC, CSP_IMG_SRC, CSP_MEDIA_SRC, CSP_FRAME_SRC, CSP_FONT_SRC, CSP_CONNECT_SRC, CSP_REPORT_URI, CSP_FRAME_ANCESTORS, + CSP_REFLECTED_XSS, // CSP_LAST_DIRECTIVE_VALUE always needs to be the last element in the enum // because we use it to calculate the size for the char* array. CSP_LAST_DIRECTIVE_VALUE }; static const char* CSPStrDirectives[] = { - "default-src", // CSP_DEFAULT_SRC = 0 - "script-src", // CSP_SCRIPT_SRC - "object-src", // CSP_OBJECT_SRC - "style-src", // CSP_STYLE_SRC - "img-src", // CSP_IMG_SRC - "media-src", // CSP_MEDIA_SRC - "frame-src", // CSP_FRAME_SRC - "font-src", // CSP_FONT_SRC - "connect-src", // CSP_CONNECT_SRC - "report-uri", // CSP_REPORT_URI - "frame-ancestors" // CSP_FRAME_ANCESTORS + "default-src", // CSP_DEFAULT_SRC = 0 + "script-src", // CSP_SCRIPT_SRC + "object-src", // CSP_OBJECT_SRC + "style-src", // CSP_STYLE_SRC + "img-src", // CSP_IMG_SRC + "media-src", // CSP_MEDIA_SRC + "frame-src", // CSP_FRAME_SRC + "font-src", // CSP_FONT_SRC + "connect-src", // CSP_CONNECT_SRC + "report-uri", // CSP_REPORT_URI + "frame-ancestors", // CSP_FRAME_ANCESTORS + "reflected-xss" // CSP_REFLECTED_XSS }; inline const char* CSP_EnumToDirective(enum CSPDirective aDir) { // Make sure all elements in enum CSPDirective got added to CSPStrDirectives. static_assert((sizeof(CSPStrDirectives) / sizeof(CSPStrDirectives[0]) == static_cast<uint32_t>(CSP_LAST_DIRECTIVE_VALUE)), "CSP_LAST_DIRECTIVE_VALUE does not match length of CSPStrDirectives");
--- a/content/base/src/nsDocument.cpp +++ b/content/base/src/nsDocument.cpp @@ -193,16 +193,17 @@ #include "mozilla/dom/AnimationTimeline.h" #include "mozilla/dom/BindingUtils.h" #include "mozilla/dom/DocumentFragment.h" #include "mozilla/dom/Event.h" #include "mozilla/dom/HTMLBodyElement.h" #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" #include "nsDOMCaretPosition.h" #include "nsIDOMHTMLTextAreaElement.h" #include "nsViewportInfo.h" #include "nsIContentPermissionPrompt.h" #include "mozilla/StaticPtr.h" @@ -10538,18 +10539,19 @@ nsIDocument::ExitFullscreen(nsIDocument* if (aRunAsync) { NS_DispatchToCurrentThread(new nsCallExitFullscreen(aDoc)); return; } nsDocument::ExitFullscreen(aDoc); } // 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) { if (XRE_GetProcessType() != GeckoProcessType_Content) { return false; } if (aDocument->GetParentDocument() != nullptr) { return false; @@ -10557,17 +10559,22 @@ HasCrossProcessParent(nsIDocument* aDocu nsPIDOMWindow* win = aDocument->GetWindow(); if (!win) { return false; } nsCOMPtr<nsIDocShell> docShell = win->GetDocShell(); if (!docShell) { return false; } - return docShell->GetIsBrowserOrApp(); + TabChild* tabChild(TabChild::GetFrom(docShell)); + if (!tabChild) { + return false; + } + + return true; } static bool CountFullscreenSubDocuments(nsIDocument* aDoc, void* aData) { if (aDoc->IsFullScreenDoc()) { uint32_t* count = static_cast<uint32_t*>(aData); (*count)++;
--- a/content/media/MediaDecoderStateMachine.cpp +++ b/content/media/MediaDecoderStateMachine.cpp @@ -30,51 +30,58 @@ #include "nsContentUtils.h" #include "MediaShutdownManager.h" #include "SharedThreadPool.h" #include "MediaTaskQueue.h" #include "nsIEventTarget.h" #include "prenv.h" #include "mozilla/Preferences.h" #include "gfx2DGlue.h" +#include "nsPrintfCString.h" #include <algorithm> namespace mozilla { using namespace mozilla::layers; using namespace mozilla::dom; using namespace mozilla::gfx; // avoid redefined macro in unified build #undef DECODER_LOG #undef VERBOSE_LOG #ifdef PR_LOGGING extern PRLogModuleInfo* gMediaDecoderLog; -#define DECODER_LOG(type, msg, ...) \ - PR_LOG(gMediaDecoderLog, type, ("Decoder=%p " msg, mDecoder.get(), ##__VA_ARGS__)) -#define VERBOSE_LOG(msg, ...) \ +#define DECODER_LOG(x, ...) \ + PR_LOG(gMediaDecoderLog, PR_LOG_DEBUG, ("Decoder=%p " x, mDecoder.get(), ##__VA_ARGS__)) +#define VERBOSE_LOG(x, ...) \ PR_BEGIN_MACRO \ if (!PR_GetEnv("MOZ_QUIET")) { \ - DECODER_LOG(PR_LOG_DEBUG, msg, ##__VA_ARGS__); \ + DECODER_LOG(x, ##__VA_ARGS__); \ } \ PR_END_MACRO -#define SAMPLE_LOG(msg, ...) \ +#define SAMPLE_LOG(x, ...) \ PR_BEGIN_MACRO \ if (PR_GetEnv("MEDIA_LOG_SAMPLES")) { \ - DECODER_LOG(PR_LOG_DEBUG, msg, ##__VA_ARGS__); \ + DECODER_LOG(x, ##__VA_ARGS__); \ } \ PR_END_MACRO #else -#define DECODER_LOG(type, msg, ...) -#define VERBOSE_LOG(msg, ...) -#define SAMPLE_LOG(msg, ...) +#define DECODER_LOG(x, ...) +#define VERBOSE_LOG(x, ...) +#define SAMPLE_LOG(x, ...) #endif +// Somehow MSVC doesn't correctly delete the comma before ##__VA_ARGS__ +// when __VA_ARGS__ expands to nothing. This is a workaround for it. +#define DECODER_WARN_HELPER(a, b) NS_WARNING b +#define DECODER_WARN(x, ...) \ + DECODER_WARN_HELPER(0, (nsPrintfCString("Decoder=%p " x, mDecoder.get(), ##__VA_ARGS__).get())) + // GetCurrentTime is defined in winbase.h as zero argument macro forwarding to // GetTickCount() and conflicts with MediaDecoderStateMachine::GetCurrentTime // implementation. With unified builds, putting this in headers is not enough. #ifdef GetCurrentTime #undef GetCurrentTime #endif // Wait this number of seconds when buffering, then leave and play @@ -595,17 +602,17 @@ MediaDecoderStateMachine::DecodeVideo() // don't skip frame when |clock time| <= |mVideoFrameEndTime| for // we are still in the safe range without underrunning video frames GetClock() > mVideoFrameEndTime && (static_cast<uint32_t>(VideoQueue().GetSize()) < LOW_VIDEO_FRAMES * mPlaybackRate))) && !HasLowUndecodedData()) { skipToNextKeyFrame = true; - DECODER_LOG(PR_LOG_DEBUG, "Skipping video decode to the next keyframe"); + DECODER_LOG("Skipping video decode to the next keyframe"); } currentTime = mState == DECODER_STATE_SEEKING ? 0 : GetMediaTime(); // Time the video decode, so that if it's slow, we can increase our low // audio threshold to reduce the chance of an audio underrun while we're // waiting for a video decode to complete. mVideoDecodeStartTime = TimeStamp::Now(); } @@ -918,17 +925,17 @@ MediaDecoderStateMachine::OnVideoDecoded TimeDuration decodeTime = TimeStamp::Now() - mVideoDecodeStartTime; if (THRESHOLD_FACTOR * DurationToUsecs(decodeTime) > mLowAudioThresholdUsecs && !HasLowUndecodedData()) { mLowAudioThresholdUsecs = std::min(THRESHOLD_FACTOR * DurationToUsecs(decodeTime), AMPLE_AUDIO_USECS); mAmpleAudioThresholdUsecs = std::max(THRESHOLD_FACTOR * mLowAudioThresholdUsecs, mAmpleAudioThresholdUsecs); - DECODER_LOG(PR_LOG_DEBUG, "Slow video decode, set mLowAudioThresholdUsecs=%lld mAmpleAudioThresholdUsecs=%lld", + DECODER_LOG("Slow video decode, set mLowAudioThresholdUsecs=%lld mAmpleAudioThresholdUsecs=%lld", mLowAudioThresholdUsecs, mAmpleAudioThresholdUsecs); } return; } case DECODER_STATE_SEEKING: { if (!mCurrentSeekTarget.IsValid()) { // We've received a sample from a previous decode. Discard it. return; @@ -978,26 +985,26 @@ void MediaDecoderStateMachine::CheckIfSeekComplete() { AssertCurrentThreadInMonitor(); const bool videoSeekComplete = IsVideoSeekComplete(); if (HasVideo() && !videoSeekComplete) { // We haven't reached the target. Ensure we have requested another sample. if (NS_FAILED(EnsureVideoDecodeTaskQueued())) { - NS_WARNING("Failed to request video during seek"); + DECODER_WARN("Failed to request video during seek"); DecodeError(); } } const bool audioSeekComplete = IsAudioSeekComplete(); if (HasAudio() && !audioSeekComplete) { // We haven't reached the target. Ensure we have requested another sample. if (NS_FAILED(EnsureAudioDecodeTaskQueued())) { - NS_WARNING("Failed to request audio during seek"); + DECODER_WARN("Failed to request audio during seek"); DecodeError(); } } SAMPLE_LOG("CheckIfSeekComplete() audioSeekComplete=%d videoSeekComplete=%d", audioSeekComplete, videoSeekComplete); if (audioSeekComplete && videoSeekComplete) { @@ -1038,17 +1045,17 @@ MediaDecoderStateMachine::CheckIfDecodeC } if (!IsVideoDecoding() && !IsAudioDecoding()) { // We've finished decoding all active streams, // so move to COMPLETED state. mState = DECODER_STATE_COMPLETED; DispatchDecodeTasksIfNeeded(); ScheduleStateMachine(); } - DECODER_LOG(PR_LOG_DEBUG, "CheckIfDecodeComplete %scompleted", + DECODER_LOG("CheckIfDecodeComplete %scompleted", ((mState == DECODER_STATE_COMPLETED) ? "" : "NOT ")); } bool MediaDecoderStateMachine::IsPlaying() { AssertCurrentThreadInMonitor(); return !mPlayStartTime.IsNull(); @@ -1082,17 +1089,17 @@ nsresult MediaDecoderStateMachine::Init( rv = mReader->Init(cloneReader); NS_ENSURE_SUCCESS(rv, rv); return NS_OK; } void MediaDecoderStateMachine::StopPlayback() { - DECODER_LOG(PR_LOG_DEBUG, "StopPlayback()"); + DECODER_LOG("StopPlayback()"); AssertCurrentThreadInMonitor(); mDecoder->NotifyPlaybackStopped(); if (IsPlaying()) { mPlayDuration = GetClock() - mStartTime; SetPlayStartTime(TimeStamp()); @@ -1125,32 +1132,32 @@ int64_t MediaDecoderStateMachine::GetCur NS_ASSERTION(mSyncPointInDecodedStream >= 0, "Should have set up sync point"); DecodedStreamData* stream = mDecoder->GetDecodedStream(); int64_t streamDelta = stream->GetLastOutputTime() - mSyncPointInMediaStream; return mSyncPointInDecodedStream + streamDelta; } void MediaDecoderStateMachine::StartPlayback() { - DECODER_LOG(PR_LOG_DEBUG, "StartPlayback()"); + DECODER_LOG("StartPlayback()"); NS_ASSERTION(!IsPlaying(), "Shouldn't be playing when StartPlayback() is called"); AssertCurrentThreadInMonitor(); if (mDecoder->CheckDecoderCanOffloadAudio()) { - DECODER_LOG(PR_LOG_DEBUG, "Offloading playback"); + DECODER_LOG("Offloading playback"); return; } mDecoder->NotifyPlaybackStarted(); SetPlayStartTime(TimeStamp::Now()); NS_ASSERTION(IsPlaying(), "Should report playing by end of StartPlayback()"); if (NS_FAILED(StartAudioThread())) { - NS_WARNING("Failed to create audio thread"); + DECODER_WARN("Failed to create audio thread"); } mDecoder->GetReentrantMonitor().NotifyAll(); mDecoder->UpdateStreamBlockingForStateMachinePlaying(); DispatchDecodeTasksIfNeeded(); } void MediaDecoderStateMachine::UpdatePlaybackPositionInternal(int64_t aTime) { @@ -1308,16 +1315,18 @@ void MediaDecoderStateMachine::SetDorman { NS_ASSERTION(NS_IsMainThread(), "Should be on main thread."); AssertCurrentThreadInMonitor(); if (!mReader) { return; } + DECODER_LOG("SetDormant=%d", aDormant); + if (aDormant) { ScheduleStateMachine(); mState = DECODER_STATE_DORMANT; mDecoder->GetReentrantMonitor().NotifyAll(); } else if ((aDormant != true) && (mState == DECODER_STATE_DORMANT)) { ScheduleStateMachine(); mStartTime = 0; mCurrentFrameTime = 0; @@ -1330,17 +1339,17 @@ void MediaDecoderStateMachine::Shutdown( { NS_ASSERTION(NS_IsMainThread(), "Should be on main thread."); // Once we've entered the shutdown state here there's no going back. ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); // Change state before issuing shutdown request to threads so those // threads can start exiting cleanly during the Shutdown call. - DECODER_LOG(PR_LOG_DEBUG, "Changed state to SHUTDOWN"); + DECODER_LOG("Changed state to SHUTDOWN"); mState = DECODER_STATE_SHUTDOWN; mScheduler->ScheduleAndShutdown(); if (mAudioSink) { mAudioSink->PrepareToShutdown(); } mDecoder->GetReentrantMonitor().NotifyAll(); } @@ -1372,40 +1381,42 @@ void MediaDecoderStateMachine::StartDeco } void MediaDecoderStateMachine::StartWaitForResources() { NS_ASSERTION(OnStateMachineThread() || OnDecodeThread(), "Should be on state machine or decode thread."); AssertCurrentThreadInMonitor(); mState = DECODER_STATE_WAIT_FOR_RESOURCES; + DECODER_LOG("StartWaitForResources"); } void MediaDecoderStateMachine::NotifyWaitingForResourcesStatusChanged() { AssertCurrentThreadInMonitor(); if (mState != DECODER_STATE_WAIT_FOR_RESOURCES || mReader->IsWaitingMediaResources()) { return; } + DECODER_LOG("NotifyWaitingForResourcesStatusChanged"); // The reader is no longer waiting for resources (say a hardware decoder), // we can now proceed to decode metadata. mState = DECODER_STATE_DECODING_METADATA; EnqueueDecodeMetadataTask(); } void MediaDecoderStateMachine::Play() { NS_ASSERTION(NS_IsMainThread(), "Should be on main thread."); // When asked to play, switch to decoding state only if // we are currently buffering. In other cases, we'll start playing anyway // when the state machine notices the decoder's state change to PLAYING. ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); if (mState == DECODER_STATE_BUFFERING) { - DECODER_LOG(PR_LOG_DEBUG, "Changed state from BUFFERING to DECODING"); + DECODER_LOG("Changed state from BUFFERING to DECODING"); mState = DECODER_STATE_DECODING; mDecodeStartTime = TimeStamp::Now(); } // Once we start playing, we don't want to minimize our prerolling, as we // assume the user is likely to want to keep playing in future. mMinimizePreroll = false; ScheduleStateMachine(); } @@ -1456,17 +1467,17 @@ void MediaDecoderStateMachine::NotifyDat void MediaDecoderStateMachine::Seek(const SeekTarget& aTarget) { NS_ASSERTION(NS_IsMainThread(), "Should be on main thread."); ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); // We need to be able to seek both at a transport level and at a media level // to seek. if (!mDecoder->IsMediaSeekable()) { - NS_WARNING("Seek() function should not be called on a non-seekable state machine"); + DECODER_WARN("Seek() function should not be called on a non-seekable state machine"); return; } // MediaDecoder::mPlayState should be SEEKING while we seek, and // in that case MediaDecoder shouldn't be calling us. NS_ASSERTION(mState != DECODER_STATE_SEEKING, "We shouldn't already be seeking"); NS_ASSERTION(mState >= DECODER_STATE_DECODING, "We should have loaded metadata"); @@ -1476,17 +1487,17 @@ void MediaDecoderStateMachine::Seek(cons NS_ASSERTION(mEndTime != -1, "Should know end time by now"); int64_t seekTime = aTarget.mTime + mStartTime; seekTime = std::min(seekTime, mEndTime); seekTime = std::max(mStartTime, seekTime); NS_ASSERTION(seekTime >= mStartTime && seekTime <= mEndTime, "Can only seek in range [0,duration]"); mSeekTarget = SeekTarget(seekTime, aTarget.mType); - DECODER_LOG(PR_LOG_DEBUG, "Changed state to SEEKING (to %lld)", mSeekTarget.mTime); + DECODER_LOG("Changed state to SEEKING (to %lld)", mSeekTarget.mTime); mState = DECODER_STATE_SEEKING; if (mDecoder->GetDecodedStream()) { mDecoder->RecreateDecodedStream(seekTime - mStartTime); } ScheduleStateMachine(); } void MediaDecoderStateMachine::StopAudioThread() @@ -1498,17 +1509,17 @@ void MediaDecoderStateMachine::StopAudio if (mStopAudioThread) { // Nothing to do, since the thread is already stopping return; } mStopAudioThread = true; mDecoder->GetReentrantMonitor().NotifyAll(); if (mAudioSink) { - DECODER_LOG(PR_LOG_DEBUG, "Shutdown audio thread"); + DECODER_LOG("Shutdown audio thread"); mAudioSink->PrepareToShutdown(); { ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor()); mAudioSink->Shutdown(); } mAudioSink = nullptr; // Now that the audio sink is dead, try sending data to our MediaStream(s). // That may have been waiting for the audio thread to stop. @@ -1526,29 +1537,29 @@ MediaDecoderStateMachine::EnqueueDecodeM return NS_OK; } mDispatchedDecodeMetadataTask = true; RefPtr<nsIRunnable> task( NS_NewRunnableMethod(this, &MediaDecoderStateMachine::CallDecodeMetadata)); nsresult rv = mDecodeTaskQueue->Dispatch(task); if (NS_FAILED(rv)) { - NS_WARNING("Dispatch ReadMetadata task failed."); + DECODER_WARN("Dispatch ReadMetadata task failed."); mDispatchedDecodeMetadataTask = false; } return rv; } void MediaDecoderStateMachine::SetReaderIdle() { #ifdef PR_LOGGING { ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); - DECODER_LOG(PR_LOG_DEBUG, "SetReaderIdle() audioQueue=%lld videoQueue=%lld", + DECODER_LOG("SetReaderIdle() audioQueue=%lld videoQueue=%lld", GetDecodedAudioDuration(), VideoQueue().Duration()); } #endif MOZ_ASSERT(OnDecodeThread()); mReader->SetIdle(); } @@ -1601,17 +1612,17 @@ MediaDecoderStateMachine::DispatchDecode needToDecodeVideo, mVideoRequestPending, needIdle); if (needIdle) { RefPtr<nsIRunnable> event = NS_NewRunnableMethod( this, &MediaDecoderStateMachine::SetReaderIdle); nsresult rv = mDecodeTaskQueue->Dispatch(event.forget()); if (NS_FAILED(rv) && mState != DECODER_STATE_SHUTDOWN) { - NS_WARNING("Failed to dispatch event to set decoder idle state"); + DECODER_WARN("Failed to dispatch event to set decoder idle state"); } } } nsresult MediaDecoderStateMachine::EnqueueDecodeSeekTask() { NS_ASSERTION(OnStateMachineThread() || OnDecodeThread(), @@ -1627,17 +1638,17 @@ MediaDecoderStateMachine::EnqueueDecodeS mSeekTarget.Reset(); mDropAudioUntilNextDiscontinuity = HasAudio(); mDropVideoUntilNextDiscontinuity = HasVideo(); RefPtr<nsIRunnable> task( NS_NewRunnableMethod(this, &MediaDecoderStateMachine::DecodeSeek)); nsresult rv = mDecodeTaskQueue->Dispatch(task); if (NS_FAILED(rv)) { - NS_WARNING("Dispatch DecodeSeek task failed."); + DECODER_WARN("Dispatch DecodeSeek task failed."); mCurrentSeekTarget.Reset(); DecodeError(); } return rv; } nsresult MediaDecoderStateMachine::DispatchAudioDecodeTaskIfNeeded() @@ -1671,17 +1682,17 @@ MediaDecoderStateMachine::EnsureAudioDec if (IsAudioDecoding() && !mAudioRequestPending) { RefPtr<nsIRunnable> task( NS_NewRunnableMethod(this, &MediaDecoderStateMachine::DecodeAudio)); nsresult rv = mDecodeTaskQueue->Dispatch(task); if (NS_SUCCEEDED(rv)) { mAudioRequestPending = true; } else { - NS_WARNING("Failed to dispatch task to decode audio"); + DECODER_WARN("Failed to dispatch task to decode audio"); } } return NS_OK; } nsresult MediaDecoderStateMachine::DispatchVideoDecodeTaskIfNeeded() @@ -1716,17 +1727,17 @@ MediaDecoderStateMachine::EnsureVideoDec if (IsVideoDecoding() && !mVideoRequestPending) { RefPtr<nsIRunnable> task( NS_NewRunnableMethod(this, &MediaDecoderStateMachine::DecodeVideo)); nsresult rv = mDecodeTaskQueue->Dispatch(task); if (NS_SUCCEEDED(rv)) { mVideoRequestPending = true; } else { - NS_WARNING("Failed to dispatch task to decode video"); + DECODER_WARN("Failed to dispatch task to decode video"); } } return NS_OK; } nsresult MediaDecoderStateMachine::StartAudioThread() @@ -1741,17 +1752,17 @@ MediaDecoderStateMachine::StartAudioThre mStopAudioThread = false; if (HasAudio() && !mAudioSink) { mAudioCompleted = false; mAudioSink = new AudioSink(this, mAudioStartTime, mInfo.mAudio, mDecoder->GetAudioChannel()); nsresult rv = mAudioSink->Init(); if (NS_FAILED(rv)) { - DECODER_LOG(PR_LOG_WARNING, "Changed state to SHUTDOWN because audio sink initialization failed"); + DECODER_WARN("Changed state to SHUTDOWN because audio sink initialization failed"); mState = DECODER_STATE_SHUTDOWN; mScheduler->ScheduleAndShutdown(); return rv; } mAudioSink->SetVolume(mVolume); mAudioSink->SetPlaybackRate(mPlaybackRate); mAudioSink->SetPreservesPitch(mPreservesPitch); @@ -1820,17 +1831,17 @@ MediaDecoderStateMachine::DecodeError() if (mState == DECODER_STATE_SHUTDOWN) { // Already shutdown. return; } // Change state to shutdown before sending error report to MediaDecoder // and the HTMLMediaElement, so that our pipeline can start exiting // cleanly during the sync dispatch below. - DECODER_LOG(PR_LOG_WARNING, "Decode error, changed state to SHUTDOWN due to error"); + DECODER_WARN("Decode error, changed state to SHUTDOWN due to error"); mState = DECODER_STATE_SHUTDOWN; mScheduler->ScheduleAndShutdown(); mDecoder->GetReentrantMonitor().NotifyAll(); // Dispatch the event to call DecodeError synchronously. This ensures // we're in shutdown state by the time we exit the decode thread. // If we just moved to shutdown state here on the decode thread, we may // cause the state machine to shutdown/free memory without closing its @@ -1847,26 +1858,26 @@ MediaDecoderStateMachine::DecodeError() void MediaDecoderStateMachine::CallDecodeMetadata() { ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); if (mState != DECODER_STATE_DECODING_METADATA) { return; } if (NS_FAILED(DecodeMetadata())) { - DECODER_LOG(PR_LOG_WARNING, "Decode metadata failed, shutting down decoder"); + DECODER_WARN("Decode metadata failed, shutting down decoder"); DecodeError(); } } nsresult MediaDecoderStateMachine::DecodeMetadata() { AssertCurrentThreadInMonitor(); NS_ASSERTION(OnDecodeThread(), "Should be on decode thread."); - DECODER_LOG(PR_LOG_DEBUG, "Decoding Media Headers"); + DECODER_LOG("Decoding Media Headers"); if (mState != DECODER_STATE_DECODING_METADATA) { return NS_ERROR_FAILURE; } nsresult res; MediaInfo info; { ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor()); @@ -1886,16 +1897,17 @@ nsresult MediaDecoderStateMachine::Decod if (NS_SUCCEEDED(res)) { mDecoder->SetMediaSeekable(mReader->IsMediaSeekable()); } mInfo = info; if (NS_FAILED(res) || (!info.HasValidMedia())) { + DECODER_WARN("ReadMetadata failed, res=%x HasValidMedia=%d", res, info.HasValidMedia()); return NS_ERROR_FAILURE; } mDecoder->StartProgressUpdates(); mGotDurationFromMetaData = (GetDuration() != -1); if (HasAudio()) { RefPtr<nsIRunnable> decodeTask( NS_NewRunnableMethod(this, &MediaDecoderStateMachine::DispatchAudioDecodeTaskIfNeeded)); @@ -1925,53 +1937,51 @@ nsresult MediaDecoderStateMachine::Decod return NS_OK; } nsresult MediaDecoderStateMachine::FinishDecodeMetadata() { AssertCurrentThreadInMonitor(); NS_ASSERTION(OnDecodeThread(), "Should be on decode thread."); - DECODER_LOG(PR_LOG_DEBUG, "Decoding Media Headers"); + DECODER_LOG("FinishDecodeMetadata"); if (mState == DECODER_STATE_SHUTDOWN) { return NS_ERROR_FAILURE; } if (!mScheduler->IsRealTime()) { const VideoData* v = VideoQueue().PeekFront(); const AudioData* a = AudioQueue().PeekFront(); int64_t startTime = std::min<int64_t>(a ? a->mTime : INT64_MAX, v ? v->mTime : INT64_MAX); if (startTime == INT64_MAX) { startTime = 0; } - DECODER_LOG(PR_LOG_DEBUG, "DecodeMetadata first video frame start %lld", - v ? v->mTime : -1); - DECODER_LOG(PR_LOG_DEBUG, "DecodeMetadata first audio frame start %lld", - a ? a->mTime : -1); + DECODER_LOG("DecodeMetadata first video frame start %lld", v ? v->mTime : -1); + DECODER_LOG("DecodeMetadata first audio frame start %lld", a ? a->mTime : -1); SetStartTime(startTime); if (VideoQueue().GetSize()) { ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor()); RenderVideoFrame(VideoQueue().PeekFront(), TimeStamp::Now()); } } NS_ASSERTION(mStartTime != -1, "Must have start time"); MOZ_ASSERT((!HasVideo() && !HasAudio()) || !(mDecoder->IsMediaSeekable() && mDecoder->IsTransportSeekable()) || mEndTime != -1, "Active seekable media should have end time"); MOZ_ASSERT(!(mDecoder->IsMediaSeekable() && mDecoder->IsTransportSeekable()) || GetDuration() != -1, "Seekable media should have duration"); - DECODER_LOG(PR_LOG_DEBUG, "Media goes from %lld to %lld (duration %lld) " - "transportSeekable=%d, mediaSeekable=%d", + DECODER_LOG("Media goes from %lld to %lld (duration %lld) " + "transportSeekable=%d, mediaSeekable=%d", mStartTime, mEndTime, GetDuration(), mDecoder->IsTransportSeekable(), mDecoder->IsMediaSeekable()); if (HasAudio() && !HasVideo()) { // We're playing audio only. We don't need to worry about slow video // decodes causing audio underruns, so don't buffer so much audio in // order to reduce memory usage. mAmpleAudioThresholdUsecs /= NO_VIDEO_AMPLE_AUDIO_DIVISOR; @@ -1981,17 +1991,17 @@ MediaDecoderStateMachine::FinishDecodeMe // Inform the element that we've loaded the metadata and the first frame. nsAutoPtr<MediaInfo> info(new MediaInfo()); *info = mInfo; nsCOMPtr<nsIRunnable> metadataLoadedEvent = new MetadataEventRunner(mDecoder, info.forget(), mMetadataTags.forget()); NS_DispatchToMainThread(metadataLoadedEvent, NS_DISPATCH_NORMAL); if (mState == DECODER_STATE_DECODING_METADATA) { - DECODER_LOG(PR_LOG_DEBUG, "Changed state from DECODING_METADATA to DECODING"); + DECODER_LOG("Changed state from DECODING_METADATA to DECODING"); StartDecoding(); } // For very short media the metadata decode can decode the entire media. // So we need to check if this has occurred, else our decode pipeline won't // run (since it doesn't need to) and we won't detect end of stream. CheckIfDecodeComplete(); @@ -2046,17 +2056,17 @@ void MediaDecoderStateMachine::DecodeSee NS_DispatchToMainThread(startEvent, NS_DISPATCH_SYNC); } if (mState != DECODER_STATE_SEEKING) { // May have shutdown while we released the monitor. return; } if (!currentTimeChanged) { - DECODER_LOG(PR_LOG_DEBUG, "Seek !currentTimeChanged..."); + DECODER_LOG("Seek !currentTimeChanged..."); mDecodeToSeekTarget = false; nsresult rv = mDecodeTaskQueue->Dispatch( NS_NewRunnableMethod(this, &MediaDecoderStateMachine::SeekCompleted)); if (NS_FAILED(rv)) { DecodeError(); } } else { // The seek target is different than the current playback position, @@ -2145,36 +2155,36 @@ MediaDecoderStateMachine::SeekCompleted( // if we need to seek again. nsCOMPtr<nsIRunnable> stopEvent; bool isLiveStream = mDecoder->GetResource()->GetLength() == -1; if (GetMediaTime() == mEndTime && !isLiveStream) { // Seeked to end of media, move to COMPLETED state. Note we don't do // this if we're playing a live stream, since the end of media will advance // once we download more data! - DECODER_LOG(PR_LOG_DEBUG, "Changed state from SEEKING (to %lld) to COMPLETED", seekTime); + DECODER_LOG("Changed state from SEEKING (to %lld) to COMPLETED", seekTime); stopEvent = NS_NewRunnableMethod(mDecoder, &MediaDecoder::SeekingStoppedAtEnd); // Explicitly set our state so we don't decode further, and so // we report playback ended to the media element. mState = DECODER_STATE_COMPLETED; DispatchDecodeTasksIfNeeded(); } else { - DECODER_LOG(PR_LOG_DEBUG, "Changed state from SEEKING (to %lld) to DECODING", seekTime); + DECODER_LOG("Changed state from SEEKING (to %lld) to DECODING", seekTime); stopEvent = NS_NewRunnableMethod(mDecoder, &MediaDecoder::SeekingStopped); StartDecoding(); } // Ensure timestamps are up to date. UpdatePlaybackPositionInternal(newCurrentTime); if (mDecoder->GetDecodedStream()) { SetSyncPointForMediaStream(); } // Try to decode another frame to detect if we're at the end... - DECODER_LOG(PR_LOG_DEBUG, "Seek completed, mCurrentFrameTime=%lld", mCurrentFrameTime); + DECODER_LOG("Seek completed, mCurrentFrameTime=%lld", mCurrentFrameTime); // Prevent changes in playback position before 'seeked' is fired for we // expect currentTime equals seek target in 'seeked' callback. mScheduler->FreezeScheduling(); { ReentrantMonitorAutoExit exitMon(mDecoder->GetReentrantMonitor()); NS_DispatchToMainThread(stopEvent, NS_DISPATCH_SYNC); } @@ -2288,16 +2298,17 @@ nsresult MediaDecoderStateMachine::RunSt // state machine) needs to finish and be released in order to allow // that. So we dispatch an event to run after this event runner has // finished and released its monitor/references. That event then will // dispatch an event to the main thread to release the decoder and // state machine. GetStateMachineThread()->Dispatch( new nsDispatchDisposeEvent(mDecoder, this), NS_DISPATCH_NORMAL); + DECODER_LOG("SHUTDOWN OK"); return NS_OK; } case DECODER_STATE_DORMANT: { if (IsPlaying()) { StopPlayback(); } FlushDecoding(); @@ -2357,24 +2368,24 @@ nsresult MediaDecoderStateMachine::RunSt bool isLiveStream = resource->GetLength() == -1; if ((isLiveStream || !mDecoder->CanPlayThrough()) && elapsed < TimeDuration::FromSeconds(mBufferingWait * mPlaybackRate) && (mQuickBuffering ? HasLowDecodedData(QUICK_BUFFERING_LOW_DATA_USECS) : HasLowUndecodedData(mBufferingWait * USECS_PER_S)) && !mDecoder->IsDataCachedToEndOfResource() && !resource->IsSuspended()) { - DECODER_LOG(PR_LOG_DEBUG, "Buffering: wait %ds, timeout in %.3lfs %s", + DECODER_LOG("Buffering: wait %ds, timeout in %.3lfs %s", mBufferingWait, mBufferingWait - elapsed.ToSeconds(), (mQuickBuffering ? "(quick exit)" : "")); ScheduleStateMachine(USECS_PER_S); return NS_OK; } else { - DECODER_LOG(PR_LOG_DEBUG, "Changed state from BUFFERING to DECODING"); - DECODER_LOG(PR_LOG_DEBUG, "Buffered for %.3lfs", (now - mBufferingStart).ToSeconds()); + DECODER_LOG("Changed state from BUFFERING to DECODING"); + DECODER_LOG("Buffered for %.3lfs", (now - mBufferingStart).ToSeconds()); StartDecoding(); } // Notify to allow blocked decoder thread to continue mDecoder->GetReentrantMonitor().NotifyAll(); UpdateReadyState(); if (mDecoder->GetState() == MediaDecoder::PLAY_STATE_PLAYING && !IsPlaying()) @@ -2693,18 +2704,17 @@ void MediaDecoderStateMachine::AdvanceFr ScheduleStateMachine(remainingTime); } nsresult MediaDecoderStateMachine::DropVideoUpToSeekTarget(VideoData* aSample) { nsAutoPtr<VideoData> video(aSample); MOZ_ASSERT(video); - DECODER_LOG(PR_LOG_DEBUG, - "DropVideoUpToSeekTarget() frame [%lld, %lld] dup=%d", + DECODER_LOG("DropVideoUpToSeekTarget() frame [%lld, %lld] dup=%d", video->mTime, video->GetEndTime(), video->mDuplicate); const int64_t target = mCurrentSeekTarget.mTime; // Duplicate handling: if we're dropping frames up the seek target, we must // be wary of Theora duplicate frames. They don't have an image, so if the // target frame is in a run of duplicates, we won't have an image to draw // after the seek. So store the last frame encountered while dropping, and // copy its Image forward onto duplicate frames, so that every frame has @@ -2717,32 +2727,30 @@ MediaDecoderStateMachine::DropVideoUpToS video->mTime, video->mDuration); video = temp; } // If the frame end time is less than the seek target, we won't want // to display this frame after the seek, so discard it. if (target >= video->GetEndTime()) { - DECODER_LOG(PR_LOG_DEBUG, - "DropVideoUpToSeekTarget() pop video frame [%lld, %lld] target=%lld", + DECODER_LOG("DropVideoUpToSeekTarget() pop video frame [%lld, %lld] target=%lld", video->mTime, video->GetEndTime(), target); mFirstVideoFrameAfterSeek = video; } else { if (target >= video->mTime && video->GetEndTime() >= target) { // The seek target lies inside this frame's time slice. Adjust the frame's // start time to match the seek target. We do this by replacing the // first frame with a shallow copy which has the new timestamp. VideoData* temp = VideoData::ShallowCopyUpdateTimestamp(video, target); video = temp; } mFirstVideoFrameAfterSeek = nullptr; - DECODER_LOG(PR_LOG_DEBUG, - "DropVideoUpToSeekTarget() found video frame [%lld, %lld] containing target=%lld", + DECODER_LOG("DropVideoUpToSeekTarget() found video frame [%lld, %lld] containing target=%lld", video->mTime, video->GetEndTime(), target); VideoQueue().PushFront(video.forget()); } return NS_OK; } @@ -2769,34 +2777,34 @@ MediaDecoderStateMachine::DropAudioUpToS if (startFrame.value() > targetFrame.value()) { // The seek target doesn't lie in the audio block just after the last // audio frames we've seen which were before the seek target. This // could have been the first audio data we've seen after seek, i.e. the // seek terminated after the seek target in the audio stream. Just // abort the audio decode-to-target, the state machine will play // silence to cover the gap. Typically this happens in poorly muxed // files. - NS_WARNING("Audio not synced after seek, maybe a poorly muxed file?"); + DECODER_WARN("Audio not synced after seek, maybe a poorly muxed file?"); AudioQueue().Push(audio.forget()); return NS_OK; } // The seek target lies somewhere in this AudioData's frames, strip off // any frames which lie before the seek target, so we'll begin playback // exactly at the seek target. NS_ASSERTION(targetFrame.value() >= startFrame.value(), "Target must at or be after data start."); NS_ASSERTION(targetFrame.value() < startFrame.value() + audio->mFrames, "Data must end after target."); int64_t framesToPrune = targetFrame.value() - startFrame.value(); if (framesToPrune > audio->mFrames) { // We've messed up somehow. Don't try to trim frames, the |frames| // variable below will overflow. - NS_WARNING("Can't prune more frames that we have!"); + DECODER_WARN("Can't prune more frames that we have!"); return NS_ERROR_FAILURE; } uint32_t frames = audio->mFrames - static_cast<uint32_t>(framesToPrune); uint32_t channels = audio->mChannels; nsAutoArrayPtr<AudioDataValue> audioData(new AudioDataValue[frames * channels]); memcpy(audioData.get(), audio->mAudioData.get() + (framesToPrune * channels), frames * channels * sizeof(AudioDataValue)); @@ -2814,34 +2822,34 @@ MediaDecoderStateMachine::DropAudioUpToS AudioQueue().PushFront(data.forget()); return NS_OK; } void MediaDecoderStateMachine::SetStartTime(int64_t aStartTimeUsecs) { NS_ASSERTION(OnDecodeThread(), "Should be on decode thread."); - DECODER_LOG(PR_LOG_DEBUG, "SetStartTime(%lld)", aStartTimeUsecs); + DECODER_LOG("SetStartTime(%lld)", aStartTimeUsecs); mStartTime = 0; if (aStartTimeUsecs != 0) { mStartTime = aStartTimeUsecs; if (mGotDurationFromMetaData) { NS_ASSERTION(mEndTime != -1, "We should have mEndTime as supplied duration here"); // We were specified a duration from a Content-Duration HTTP header. // Adjust mEndTime so that mEndTime-mStartTime matches the specified // duration. mEndTime = mStartTime + mEndTime; } } // Set the audio start time to be start of media. If this lies before the // first actual audio frame we have, we'll inject silence during playback // to ensure the audio starts at the correct time. mAudioStartTime = mStartTime; - DECODER_LOG(PR_LOG_DEBUG, "Set media start time to %lld", mStartTime); + DECODER_LOG("Set media start time to %lld", mStartTime); } void MediaDecoderStateMachine::UpdateReadyState() { AssertCurrentThreadInMonitor(); MediaDecoderOwner::NextFrameStatus nextFrameStatus = GetNextFrameStatus(); if (nextFrameStatus == mLastFrameStatus) { return; @@ -2900,21 +2908,21 @@ void MediaDecoderStateMachine::StartBuff // there might be pending main-thread events, such as "data // received" notifications, that mean we're not actually still // buffering by the time this runnable executes. So instead // we just trigger UpdateReadyStateForData; when it runs, it // will check the current state and decide whether to tell // the element we're buffering or not. UpdateReadyState(); mState = DECODER_STATE_BUFFERING; - DECODER_LOG(PR_LOG_DEBUG, "Changed state from DECODING to BUFFERING, decoded for %.3lfs", + DECODER_LOG("Changed state from DECODING to BUFFERING, decoded for %.3lfs", decodeDuration.ToSeconds()); #ifdef PR_LOGGING MediaDecoder::Statistics stats = mDecoder->GetStatistics(); - DECODER_LOG(PR_LOG_DEBUG, "Playback rate: %.1lfKB/s%s download rate: %.1lfKB/s%s", + DECODER_LOG("Playback rate: %.1lfKB/s%s download rate: %.1lfKB/s%s", stats.mPlaybackRate/1024, stats.mPlaybackRateReliable ? "" : " (unreliable)", stats.mDownloadRate/1024, stats.mDownloadRateReliable ? "" : " (unreliable)"); #endif } nsresult MediaDecoderStateMachine::GetBuffered(dom::TimeRanges* aBuffered) { MediaResource* resource = mDecoder->GetResource(); NS_ENSURE_TRUE(resource, NS_ERROR_FAILURE); @@ -3075,8 +3083,10 @@ void MediaDecoderStateMachine::OnAudioSi mDecoder->GetReentrantMonitor().NotifyAll(); } } // namespace mozilla // avoid redefined macro in unified build #undef DECODER_LOG #undef VERBOSE_LOG +#undef DECODER_WARN +#undef DECODER_WARN_HELPER
--- a/content/media/fmp4/MP4Reader.cpp +++ b/content/media/fmp4/MP4Reader.cpp @@ -565,17 +565,17 @@ MP4Reader::Decode(TrackType aTrack) data.mMonitor.Wait(); } if (data.mError || (data.mEOS && data.mDrainComplete)) { break; } } data.mMonitor.AssertCurrentThreadOwns(); - bool rv = !(data.mEOS || data.mError); + bool rv = !(data.mDrainComplete || data.mError); data.mMonitor.Unlock(); return rv; } nsresult MP4Reader::ResetDecode() { Flush(kAudio);
--- a/content/media/gmp/GMPChild.cpp +++ b/content/media/gmp/GMPChild.cpp @@ -44,49 +44,60 @@ GMPChild::GMPChild() { } GMPChild::~GMPChild() { } static bool -GetPluginBinaryPath(const std::string& aPluginPath, - nsCString &aPluginBinaryPath) +GetPluginBinaryFile(const std::string& aPluginPath, + nsCOMPtr<nsIFile>& aLibFile) { nsDependentCString pluginPath(aPluginPath.c_str()); - nsCOMPtr<nsIFile> libFile; - nsresult rv = NS_NewNativeLocalFile(pluginPath, true, getter_AddRefs(libFile)); + nsresult rv = NS_NewLocalFile(NS_ConvertUTF8toUTF16(pluginPath), + true, getter_AddRefs(aLibFile)); if (NS_FAILED(rv)) { return false; } nsAutoString leafName; - if (NS_FAILED(libFile->GetLeafName(leafName))) { + if (NS_FAILED(aLibFile->GetLeafName(leafName))) { return false; } nsAutoString baseName(Substring(leafName, 4, leafName.Length() - 1)); #if defined(XP_MACOSX) nsAutoString binaryName = NS_LITERAL_STRING("lib") + baseName + NS_LITERAL_STRING(".dylib"); #elif defined(OS_POSIX) nsAutoString binaryName = NS_LITERAL_STRING("lib") + baseName + NS_LITERAL_STRING(".so"); #elif defined(XP_WIN) nsAutoString binaryName = baseName + NS_LITERAL_STRING(".dll"); #else #error not defined #endif - libFile->AppendRelativePath(binaryName); + aLibFile->AppendRelativePath(binaryName); + return true; +} + +#if defined(XP_MACOSX) && defined(MOZ_GMP_SANDBOX) +static bool +GetPluginBinaryPath(const std::string& aPluginPath, + nsCString &aPluginBinaryPath) +{ + nsCOMPtr<nsIFile> libFile; + if (!GetPluginBinaryFile(aPluginPath, libFile)) { + return false; + } libFile->GetNativePath(aPluginBinaryPath); return true; } -#if defined(XP_MACOSX) && defined(MOZ_GMP_SANDBOX) void GMPChild::OnChannelConnected(int32_t aPid) { MacSandboxInfo info; info.type = MacSandboxType_Plugin; info.pluginInfo.type = MacSandboxPluginType_GMPlugin_Default; info.pluginInfo.pluginPath.Assign(mPluginPath.c_str()); @@ -142,33 +153,39 @@ GMPChild::Init(const std::string& aPlugi #endif return LoadPluginLibrary(aPluginPath); } bool GMPChild::LoadPluginLibrary(const std::string& aPluginPath) { +#if defined(XP_MACOSX) && defined(MOZ_GMP_SANDBOX) nsAutoCString nativePath; -#if defined(XP_MACOSX) && defined(MOZ_GMP_SANDBOX) nativePath.Assign(mPluginBinaryPath); + + mLib = PR_LoadLibrary(nativePath.get()); #else - if (!GetPluginBinaryPath(aPluginPath, nativePath)) { + nsCOMPtr<nsIFile> libFile; + if (!GetPluginBinaryFile(aPluginPath, libFile)) { return false; } -#endif +#if defined(XP_LINUX) && defined(MOZ_GMP_SANDBOX) + nsAutoCString nativePath; + libFile->GetNativePath(nativePath); -#if defined(XP_LINUX) && defined(MOZ_GMP_SANDBOX) // Enable sandboxing here -- we know the plugin file's path, but // this process's execution hasn't been affected by its content yet. MOZ_ASSERT(mozilla::CanSandboxMediaPlugin()); mozilla::SetMediaPluginSandbox(nativePath.get()); -#endif +#endif // XP_LINUX && MOZ_GMP_SANDBOX - mLib = PR_LoadLibrary(nativePath.get()); + libFile->Load(&mLib); +#endif // XP_MACOSX && MOZ_GMP_SANDBOX + if (!mLib) { return false; } GMPInitFunc initFunc = reinterpret_cast<GMPInitFunc>(PR_FindFunctionSymbol(mLib, "GMPInit")); if (!initFunc) { return false; }
--- a/content/media/gmp/GMPParent.cpp +++ b/content/media/gmp/GMPParent.cpp @@ -114,24 +114,24 @@ GMPParent::Crash() nsresult GMPParent::LoadProcess() { MOZ_ASSERT(mDirectory, "Plugin directory cannot be NULL!"); MOZ_ASSERT(GMPThread() == NS_GetCurrentThread()); MOZ_ASSERT(mState == GMPStateNotLoaded); - nsAutoCString path; - if (NS_FAILED(mDirectory->GetNativePath(path))) { + nsAutoString path; + if (NS_FAILED(mDirectory->GetPath(path))) { return NS_ERROR_FAILURE; } LOGD(("%s::%s: %p for %s", __CLASS__, __FUNCTION__, this, path.get())); if (!mProcess) { - mProcess = new GMPProcessParent(path.get()); + mProcess = new GMPProcessParent(NS_ConvertUTF16toUTF8(path).get()); if (!mProcess->Launch(30 * 1000)) { mProcess->Delete(); mProcess = nullptr; return NS_ERROR_FAILURE; } bool opened = Open(mProcess->GetChannel(), mProcess->GetChildProcessHandle()); if (!opened) {
--- a/content/media/omx/MediaCodecReader.cpp +++ b/content/media/omx/MediaCodecReader.cpp @@ -23,138 +23,153 @@ #include <stagefright/MetaData.h> #include <stagefright/Utils.h> #include "mozilla/TimeStamp.h" #include "gfx2DGlue.h" #include "MediaStreamSource.h" +#include "MediaTaskQueue.h" +#include "nsThreadUtils.h" #include "ImageContainer.h" +#include "SharedThreadPool.h" #include "VideoFrameContainer.h" using namespace android; namespace mozilla { enum { kNotifyCodecReserved = 'core', kNotifyCodecCanceled = 'coca', }; static const int64_t sInvalidDurationUs = INT64_C(-1); static const int64_t sInvalidTimestampUs = INT64_C(-1); -// Try not to spend more than this much time (in seconds) in a single call to DecodeAudioData. +// Try not to spend more than this much time (in seconds) in a single call +// to GetCodecOutputData. static const double sMaxAudioDecodeDurationS = 0.1; -// Try not to spend more than this much time (in seconds) in a single call to DecodeVideoFrame. static const double sMaxVideoDecodeDurationS = 0.1; static CheckedUint32 sInvalidInputIndex = INT32_C(-1); inline bool IsValidDurationUs(int64_t aDuration) { return aDuration >= INT64_C(0); } inline bool IsValidTimestampUs(int64_t aTimestamp) { return aTimestamp >= INT64_C(0); } -MediaCodecReader::MessageHandler::MessageHandler(MediaCodecReader *aReader) +MediaCodecReader::MessageHandler::MessageHandler(MediaCodecReader* aReader) : mReader(aReader) { } MediaCodecReader::MessageHandler::~MessageHandler() { mReader = nullptr; } void -MediaCodecReader::MessageHandler::onMessageReceived(const android::sp<android::AMessage> &aMessage) +MediaCodecReader::MessageHandler::onMessageReceived( + const sp<AMessage>& aMessage) { - if (mReader != nullptr) { + if (mReader) { mReader->onMessageReceived(aMessage); } } -MediaCodecReader::VideoResourceListener::VideoResourceListener(MediaCodecReader *aReader) +MediaCodecReader::VideoResourceListener::VideoResourceListener( + MediaCodecReader* aReader) : mReader(aReader) { } MediaCodecReader::VideoResourceListener::~VideoResourceListener() { mReader = nullptr; } void MediaCodecReader::VideoResourceListener::codecReserved() { - if (mReader != nullptr) { + if (mReader) { mReader->codecReserved(mReader->mVideoTrack); } } void MediaCodecReader::VideoResourceListener::codecCanceled() { - if (mReader != nullptr) { + if (mReader) { mReader->codecCanceled(mReader->mVideoTrack); } } bool -MediaCodecReader::TrackInputCopier::Copy(MediaBuffer* aSourceBuffer, sp<ABuffer> aCodecBuffer) +MediaCodecReader::TrackInputCopier::Copy(MediaBuffer* aSourceBuffer, + sp<ABuffer> aCodecBuffer) { if (aSourceBuffer == nullptr || aCodecBuffer == nullptr || aSourceBuffer->range_length() > aCodecBuffer->capacity()) { return false; } aCodecBuffer->setRange(0, aSourceBuffer->range_length()); - memcpy(aCodecBuffer->data(), aSourceBuffer->data() + aSourceBuffer->range_offset(), aSourceBuffer->range_length()); + memcpy(aCodecBuffer->data(), + (uint8_t*)aSourceBuffer->data() + aSourceBuffer->range_offset(), + aSourceBuffer->range_length()); return true; } MediaCodecReader::Track::Track() : mDurationUs(INT64_C(0)) , mInputIndex(sInvalidInputIndex) , mInputEndOfStream(false) + , mOutputEndOfStream(false) , mSeekTimeUs(sInvalidTimestampUs) , mFlushed(false) + , mDiscontinuity(false) + , mTaskQueue(nullptr) { } // Append the value of |kKeyValidSamples| to the end of each vorbis buffer. // https://github.com/mozilla-b2g/platform_frameworks_av/blob/master/media/libstagefright/OMXCodec.cpp#L3128 // https://github.com/mozilla-b2g/platform_frameworks_av/blob/master/media/libstagefright/NuMediaExtractor.cpp#L472 bool -MediaCodecReader::VorbisInputCopier::Copy(MediaBuffer* aSourceBuffer, sp<ABuffer> aCodecBuffer) +MediaCodecReader::VorbisInputCopier::Copy(MediaBuffer* aSourceBuffer, + sp<ABuffer> aCodecBuffer) { if (aSourceBuffer == nullptr || aCodecBuffer == nullptr || aSourceBuffer->range_length() + sizeof(int32_t) > aCodecBuffer->capacity()) { return false; } int32_t numPageSamples = 0; if (!aSourceBuffer->meta_data()->findInt32(kKeyValidSamples, &numPageSamples)) { numPageSamples = -1; } aCodecBuffer->setRange(0, aSourceBuffer->range_length() + sizeof(int32_t)); - memcpy(aCodecBuffer->data(), aSourceBuffer->data() + aSourceBuffer->range_offset(), aSourceBuffer->range_length()); - memcpy(aCodecBuffer->data() + aSourceBuffer->range_length(), &numPageSamples, sizeof(numPageSamples)); + memcpy(aCodecBuffer->data(), + (uint8_t*)aSourceBuffer->data() + aSourceBuffer->range_offset(), + aSourceBuffer->range_length()); + memcpy(aCodecBuffer->data() + aSourceBuffer->range_length(), + &numPageSamples, sizeof(numPageSamples)); return true; } MediaCodecReader::AudioTrack::AudioTrack() { } @@ -215,200 +230,175 @@ MediaCodecReader::ReleaseMediaResources( } void MediaCodecReader::Shutdown() { ReleaseResources(); } -bool -MediaCodecReader::DecodeAudioData() +void +MediaCodecReader::DispatchAudioTask() +{ + if (mAudioTrack.mTaskQueue && mAudioTrack.mTaskQueue->IsEmpty()) { + RefPtr<nsIRunnable> task = + NS_NewRunnableMethod(this, + &MediaCodecReader::DecodeAudioDataTask); + mAudioTrack.mTaskQueue->Dispatch(task); + } +} + +void +MediaCodecReader::DispatchVideoTask(int64_t aTimeThreshold) +{ + if (mVideoTrack.mTaskQueue && mVideoTrack.mTaskQueue->IsEmpty()) { + RefPtr<nsIRunnable> task = + NS_NewRunnableMethodWithArg<int64_t>(this, + &MediaCodecReader::DecodeVideoFrameTask, + aTimeThreshold); + mVideoTrack.mTaskQueue->Dispatch(task); + } +} + +void +MediaCodecReader::RequestAudioData() { - MOZ_ASSERT(mDecoder->OnDecodeThread(), "Should be on decode thread."); + MOZ_ASSERT(GetTaskQueue()->IsCurrentThreadIn()); + MOZ_ASSERT(HasAudio()); + if (CheckAudioResources()) { + DispatchAudioTask(); + } +} + +void +MediaCodecReader::RequestVideoData(bool aSkipToNextKeyframe, + int64_t aTimeThreshold) +{ + MOZ_ASSERT(GetTaskQueue()->IsCurrentThreadIn()); + MOZ_ASSERT(HasVideo()); - if (mAudioTrack.mCodec == nullptr || !mAudioTrack.mCodec->allocated()) { + int64_t threshold = sInvalidTimestampUs; + if (aSkipToNextKeyframe && IsValidTimestampUs(aTimeThreshold)) { + mVideoTrack.mTaskQueue->Flush(); + threshold = aTimeThreshold; + } + if (CheckVideoResources()) { + DispatchVideoTask(threshold); + } +} + +bool +MediaCodecReader::DecodeAudioDataSync() +{ + if (mAudioTrack.mCodec == nullptr || !mAudioTrack.mCodec->allocated() || + mAudioTrack.mOutputEndOfStream) { return false; } // Get one audio output data from MediaCodec CodecBufferInfo bufferInfo; status_t status; - TimeStamp timeout = TimeStamp::Now() + TimeDuration::FromSeconds(sMaxAudioDecodeDurationS); + TimeStamp timeout = TimeStamp::Now() + + TimeDuration::FromSeconds(sMaxAudioDecodeDurationS); while (true) { - if (timeout < TimeStamp::Now()) { - return true; // Try it again later. - } - status = GetCodecOutputData(mAudioTrack, bufferInfo, sInvalidTimestampUs, timeout); + // Try to fill more input buffers and then get one output buffer. + // FIXME: use callback from MediaCodec + FillCodecInputData(mAudioTrack); + + status = GetCodecOutputData(mAudioTrack, bufferInfo, sInvalidTimestampUs, + timeout); if (status == OK || status == ERROR_END_OF_STREAM) { break; } else if (status == -EAGAIN) { - return true; // Try it again later. + if (TimeStamp::Now() > timeout) { + // Don't let this loop run for too long. Try it again later. + if (CheckAudioResources()) { + DispatchAudioTask(); + } + return true; + } + continue; // Try it again now. } else if (status == INFO_FORMAT_CHANGED) { if (UpdateAudioInfo()) { continue; // Try it again now. } else { return false; } } else { return false; } } - bool result = true; - - if (bufferInfo.mBuffer != nullptr && bufferInfo.mSize > 0 && bufferInfo.mBuffer->data() != nullptr) { + bool result = false; + if (bufferInfo.mBuffer != nullptr && bufferInfo.mSize > 0 && + bufferInfo.mBuffer->data() != nullptr) { // This is the approximate byte position in the stream. int64_t pos = mDecoder->GetResource()->Tell(); - uint32_t frames = bufferInfo.mSize / (mInfo.mAudio.mChannels * sizeof(AudioDataValue)); + uint32_t frames = bufferInfo.mSize / + (mInfo.mAudio.mChannels * sizeof(AudioDataValue)); result = mAudioCompactor.Push( - pos, - bufferInfo.mTimeUs, - mInfo.mAudio.mRate, - frames, - mInfo.mAudio.mChannels, - AudioCompactor::NativeCopy( - bufferInfo.mBuffer->data() + bufferInfo.mOffset, - bufferInfo.mSize, - mInfo.mAudio.mChannels)); + pos, + bufferInfo.mTimeUs, + mInfo.mAudio.mRate, + frames, + mInfo.mAudio.mChannels, + AudioCompactor::NativeCopy( + bufferInfo.mBuffer->data() + bufferInfo.mOffset, + bufferInfo.mSize, + mInfo.mAudio.mChannels)); } + if ((bufferInfo.mFlags & MediaCodec::BUFFER_FLAG_EOS) || + (status == ERROR_END_OF_STREAM)) { + AudioQueue().Finish(); + } mAudioTrack.mCodec->releaseOutputBuffer(bufferInfo.mIndex); - if (status == ERROR_END_OF_STREAM) { - return false; - } - return result; } bool -MediaCodecReader::DecodeVideoFrame(bool &aKeyframeSkip, int64_t aTimeThreshold) +MediaCodecReader::DecodeAudioDataTask() { - MOZ_ASSERT(mDecoder->OnDecodeThread(), "Should be on decode thread."); - - if (mVideoTrack.mCodec == nullptr || !mVideoTrack.mCodec->allocated()) { - return false; - } - - int64_t threshold = sInvalidTimestampUs; - if (aKeyframeSkip && IsValidTimestampUs(aTimeThreshold)) { - threshold = aTimeThreshold; - } - - // Get one video output data from MediaCodec - CodecBufferInfo bufferInfo; - status_t status; - TimeStamp timeout = TimeStamp::Now() + TimeDuration::FromSeconds(sMaxVideoDecodeDurationS); - while (true) { - if (timeout < TimeStamp::Now()) { - return true; // Try it again later. - } - status = GetCodecOutputData(mVideoTrack, bufferInfo, threshold, timeout); - if (status == OK || status == ERROR_END_OF_STREAM) { - break; - } else if (status == -EAGAIN) { - return true; // Try it again later. - } else if (status == INFO_FORMAT_CHANGED) { - if (UpdateVideoInfo()) { - continue; // Try it again now. - } else { - return false; + bool result = DecodeAudioDataSync(); + if (AudioQueue().GetSize() > 0) { + AudioData* a = AudioQueue().PopFront(); + if (a) { + if (mAudioTrack.mDiscontinuity) { + a->mDiscontinuity = true; + mAudioTrack.mDiscontinuity = false; } - } else { - return false; + GetCallback()->OnAudioDecoded(a); } } - - bool result = true; - - if (bufferInfo.mBuffer != nullptr && bufferInfo.mSize > 0 && bufferInfo.mBuffer->data() != nullptr) { - uint8_t *yuv420p_buffer = bufferInfo.mBuffer->data(); - int32_t stride = mVideoTrack.mStride; - int32_t slice_height = mVideoTrack.mSliceHeight; - - // Converts to OMX_COLOR_FormatYUV420Planar - if (mVideoTrack.mColorFormat != OMX_COLOR_FormatYUV420Planar) { - ARect crop; - crop.top = 0; - crop.bottom = mVideoTrack.mHeight; - crop.left = 0; - crop.right = mVideoTrack.mWidth; - - yuv420p_buffer = GetColorConverterBuffer(mVideoTrack.mWidth, mVideoTrack.mHeight); - if (mColorConverter.convertDecoderOutputToI420( - bufferInfo.mBuffer->data(), mVideoTrack.mWidth, mVideoTrack.mHeight, crop, yuv420p_buffer) != OK) { - mVideoTrack.mCodec->releaseOutputBuffer(bufferInfo.mIndex); - NS_WARNING("Unable to convert color format"); - return false; - } - - stride = mVideoTrack.mWidth; - slice_height = mVideoTrack.mHeight; - } - - size_t yuv420p_y_size = stride * slice_height; - size_t yuv420p_u_size = ((stride + 1) / 2) * ((slice_height + 1) / 2); - uint8_t *yuv420p_y = yuv420p_buffer; - uint8_t *yuv420p_u = yuv420p_y + yuv420p_y_size; - uint8_t *yuv420p_v = yuv420p_u + yuv420p_u_size; - - // This is the approximate byte position in the stream. - int64_t pos = mDecoder->GetResource()->Tell(); + if (AudioQueue().AtEndOfStream()) { + GetCallback()->OnAudioEOS(); + } + return result; +} - VideoData::YCbCrBuffer b; - b.mPlanes[0].mData = yuv420p_y; - b.mPlanes[0].mWidth = mVideoTrack.mWidth; - b.mPlanes[0].mHeight = mVideoTrack.mHeight; - b.mPlanes[0].mStride = stride; - b.mPlanes[0].mOffset = 0; - b.mPlanes[0].mSkip = 0; - - b.mPlanes[1].mData = yuv420p_u; - b.mPlanes[1].mWidth = (mVideoTrack.mWidth + 1) / 2; - b.mPlanes[1].mHeight = (mVideoTrack.mHeight + 1) / 2; - b.mPlanes[1].mStride = (stride + 1) / 2; - b.mPlanes[1].mOffset = 0; - b.mPlanes[1].mSkip = 0; - - b.mPlanes[2].mData = yuv420p_v; - b.mPlanes[2].mWidth =(mVideoTrack.mWidth + 1) / 2; - b.mPlanes[2].mHeight = (mVideoTrack.mHeight + 1) / 2; - b.mPlanes[2].mStride = (stride + 1) / 2; - b.mPlanes[2].mOffset = 0; - b.mPlanes[2].mSkip = 0; - - VideoData *v = VideoData::Create( - mInfo.mVideo, - mDecoder->GetImageContainer(), - pos, - bufferInfo.mTimeUs, - 1, // We don't know the duration. - b, - bufferInfo.mFlags & MediaCodec::BUFFER_FLAG_SYNCFRAME, - -1, - mVideoTrack.mRelativePictureRect); - - if (v != nullptr) { - result = true; - mVideoQueue.Push(v); - aKeyframeSkip = false; - } else { - NS_WARNING("Unable to create VideoData"); +bool +MediaCodecReader::DecodeVideoFrameTask(int64_t aTimeThreshold) +{ + bool result = DecodeVideoFrameSync(aTimeThreshold); + if (VideoQueue().GetSize() > 0) { + VideoData* v = VideoQueue().PopFront(); + if (v) { + if (mVideoTrack.mDiscontinuity) { + v->mDiscontinuity = true; + mVideoTrack.mDiscontinuity = false; + } + GetCallback()->OnVideoDecoded(v); } } - - mVideoTrack.mCodec->releaseOutputBuffer(bufferInfo.mIndex); - - if (status == ERROR_END_OF_STREAM) { - return false; + if (VideoQueue().AtEndOfStream()) { + GetCallback()->OnVideoEOS(); } - return result; } bool MediaCodecReader::HasAudio() { return mInfo.mAudio.mHasAudio; } @@ -447,137 +437,297 @@ MediaCodecReader::ReadMetadata(MediaInfo return NS_ERROR_FAILURE; } if (!UpdateVideoInfo()) { return NS_ERROR_FAILURE; } // Set the total duration (the max of the audio and video track). - int64_t duration = mAudioTrack.mDurationUs > mVideoTrack.mDurationUs - ? mAudioTrack.mDurationUs - : mVideoTrack.mDurationUs; + int64_t duration = mAudioTrack.mDurationUs > mVideoTrack.mDurationUs ? + mAudioTrack.mDurationUs : mVideoTrack.mDurationUs; if (duration >= 0LL) { ReentrantMonitorAutoEnter mon(mDecoder->GetReentrantMonitor()); mDecoder->SetMediaDuration(duration); } // Video track's frame sizes will not overflow. Activate the video track. VideoFrameContainer* container = mDecoder->GetVideoFrameContainer(); if (container) { container->SetCurrentFrame( - gfxIntSize(mInfo.mVideo.mDisplay.width, mInfo.mVideo.mDisplay.height), - nullptr, - mozilla::TimeStamp::Now()); + gfxIntSize(mInfo.mVideo.mDisplay.width, mInfo.mVideo.mDisplay.height), + nullptr, + mozilla::TimeStamp::Now()); } *aInfo = mInfo; *aTags = nullptr; return NS_OK; } nsresult +MediaCodecReader::ResetDecode() +{ + if (CheckAudioResources()) { + mAudioTrack.mTaskQueue->Flush(); + FlushCodecData(mAudioTrack); + mAudioTrack.mDiscontinuity = true; + } + if (CheckVideoResources()) { + mVideoTrack.mTaskQueue->Flush(); + FlushCodecData(mVideoTrack); + mVideoTrack.mDiscontinuity = true; + } + + return MediaDecoderReader::ResetDecode(); +} + +bool +MediaCodecReader::DecodeVideoFrameSync(int64_t aTimeThreshold) +{ + if (mVideoTrack.mCodec == nullptr || !mVideoTrack.mCodec->allocated() || + mVideoTrack.mOutputEndOfStream) { + return false; + } + + // Get one video output data from MediaCodec + CodecBufferInfo bufferInfo; + status_t status; + TimeStamp timeout = TimeStamp::Now() + + TimeDuration::FromSeconds(sMaxVideoDecodeDurationS); + while (true) { + // Try to fill more input buffers and then get one output buffer. + // FIXME: use callback from MediaCodec + FillCodecInputData(mVideoTrack); + + status = GetCodecOutputData(mVideoTrack, bufferInfo, aTimeThreshold, + timeout); + if (status == OK || status == ERROR_END_OF_STREAM) { + break; + } else if (status == -EAGAIN) { + if (TimeStamp::Now() > timeout) { + // Don't let this loop run for too long. Try it again later. + if (CheckVideoResources()) { + DispatchVideoTask(aTimeThreshold); + } + return true; + } + continue; // Try it again now. + } else if (status == INFO_FORMAT_CHANGED) { + if (UpdateVideoInfo()) { + continue; // Try it again now. + } else { + return false; + } + } else { + return false; + } + } + + bool result = false; + if (bufferInfo.mBuffer != nullptr && bufferInfo.mSize > 0 && + bufferInfo.mBuffer->data() != nullptr) { + uint8_t* yuv420p_buffer = bufferInfo.mBuffer->data(); + int32_t stride = mVideoTrack.mStride; + int32_t slice_height = mVideoTrack.mSliceHeight; + + // Converts to OMX_COLOR_FormatYUV420Planar + if (mVideoTrack.mColorFormat != OMX_COLOR_FormatYUV420Planar) { + ARect crop; + crop.top = 0; + crop.bottom = mVideoTrack.mHeight; + crop.left = 0; + crop.right = mVideoTrack.mWidth; + + yuv420p_buffer = GetColorConverterBuffer(mVideoTrack.mWidth, + mVideoTrack.mHeight); + if (mColorConverter.convertDecoderOutputToI420( + bufferInfo.mBuffer->data(), mVideoTrack.mWidth, mVideoTrack.mHeight, + crop, yuv420p_buffer) != OK) { + mVideoTrack.mCodec->releaseOutputBuffer(bufferInfo.mIndex); + NS_WARNING("Unable to convert color format"); + return false; + } + + stride = mVideoTrack.mWidth; + slice_height = mVideoTrack.mHeight; + } + + size_t yuv420p_y_size = stride * slice_height; + size_t yuv420p_u_size = ((stride + 1) / 2) * ((slice_height + 1) / 2); + uint8_t* yuv420p_y = yuv420p_buffer; + uint8_t* yuv420p_u = yuv420p_y + yuv420p_y_size; + uint8_t* yuv420p_v = yuv420p_u + yuv420p_u_size; + + // This is the approximate byte position in the stream. + int64_t pos = mDecoder->GetResource()->Tell(); + + VideoData::YCbCrBuffer b; + b.mPlanes[0].mData = yuv420p_y; + b.mPlanes[0].mWidth = mVideoTrack.mWidth; + b.mPlanes[0].mHeight = mVideoTrack.mHeight; + b.mPlanes[0].mStride = stride; + b.mPlanes[0].mOffset = 0; + b.mPlanes[0].mSkip = 0; + + b.mPlanes[1].mData = yuv420p_u; + b.mPlanes[1].mWidth = (mVideoTrack.mWidth + 1) / 2; + b.mPlanes[1].mHeight = (mVideoTrack.mHeight + 1) / 2; + b.mPlanes[1].mStride = (stride + 1) / 2; + b.mPlanes[1].mOffset = 0; + b.mPlanes[1].mSkip = 0; + + b.mPlanes[2].mData = yuv420p_v; + b.mPlanes[2].mWidth =(mVideoTrack.mWidth + 1) / 2; + b.mPlanes[2].mHeight = (mVideoTrack.mHeight + 1) / 2; + b.mPlanes[2].mStride = (stride + 1) / 2; + b.mPlanes[2].mOffset = 0; + b.mPlanes[2].mSkip = 0; + + VideoData *v = VideoData::Create( + mInfo.mVideo, + mDecoder->GetImageContainer(), + pos, + bufferInfo.mTimeUs, + 1, // We don't know the duration. + b, + bufferInfo.mFlags & MediaCodec::BUFFER_FLAG_SYNCFRAME, + -1, + mVideoTrack.mRelativePictureRect); + + if (v) { + result = true; + VideoQueue().Push(v); + } else { + NS_WARNING("Unable to create VideoData"); + } + } + + if ((bufferInfo.mFlags & MediaCodec::BUFFER_FLAG_EOS) || + (status == ERROR_END_OF_STREAM)) { + VideoQueue().Finish(); + } + mVideoTrack.mCodec->releaseOutputBuffer(bufferInfo.mIndex); + + return result; +} + +nsresult MediaCodecReader::Seek(int64_t aTime, int64_t aStartTime, int64_t aEndTime, int64_t aCurrentTime) { MOZ_ASSERT(mDecoder->OnDecodeThread(), "Should be on decode thread."); - VideoFrameContainer* videoframe = mDecoder->GetVideoFrameContainer(); - if (videoframe != nullptr) { - mozilla::layers::ImageContainer *image = videoframe->GetImageContainer(); - if (image != nullptr) { - image->ClearAllImagesExceptFront(); - } - } - + mVideoTrack.mSeekTimeUs = aTime; + mAudioTrack.mSeekTimeUs = aTime; + mVideoTrack.mInputEndOfStream = false; + mVideoTrack.mOutputEndOfStream = false; mAudioTrack.mInputEndOfStream = false; - mVideoTrack.mInputEndOfStream = false; - - mAudioTrack.mSeekTimeUs = aTime; - mVideoTrack.mSeekTimeUs = aTime; - + mAudioTrack.mOutputEndOfStream = false; mAudioTrack.mFlushed = false; mVideoTrack.mFlushed = false; - // Regulate the seek time to the closest sync point of video data. - if (HasVideo() && mVideoTrack.mSource != nullptr) { - MediaBuffer *source_buffer = nullptr; + if (CheckVideoResources()) { + VideoFrameContainer* videoframe = mDecoder->GetVideoFrameContainer(); + if (videoframe) { + layers::ImageContainer* image = videoframe->GetImageContainer(); + if (image) { + image->ClearAllImagesExceptFront(); + } + } + + MediaBuffer* source_buffer = nullptr; MediaSource::ReadOptions options; + int64_t timestamp = sInvalidTimestampUs; options.setSeekTo(aTime, MediaSource::ReadOptions::SEEK_PREVIOUS_SYNC); - if (mVideoTrack.mSource->read(&source_buffer, &options) != OK || source_buffer == nullptr) { + if (mVideoTrack.mSource->read(&source_buffer, &options) != OK || + source_buffer == nullptr) { return NS_ERROR_FAILURE; } sp<MetaData> format = source_buffer->meta_data(); if (format != nullptr) { - int64_t timestamp = sInvalidTimestampUs; - if (format->findInt64(kKeyTime, ×tamp) && IsValidTimestampUs(timestamp)) { + if (format->findInt64(kKeyTime, ×tamp) && + IsValidTimestampUs(timestamp)) { + mVideoTrack.mSeekTimeUs = timestamp; mAudioTrack.mSeekTimeUs = timestamp; - mVideoTrack.mSeekTimeUs = timestamp; } format = nullptr; } source_buffer->release(); + + MOZ_ASSERT(mVideoTrack.mTaskQueue->IsEmpty()); + DispatchVideoTask(mVideoTrack.mSeekTimeUs); + + if (CheckAudioResources()) { + MOZ_ASSERT(mAudioTrack.mTaskQueue->IsEmpty()); + DispatchAudioTask(); + } + } else if (CheckAudioResources()) {// Audio only + MOZ_ASSERT(mAudioTrack.mTaskQueue->IsEmpty()); + DispatchAudioTask(); } - return NS_OK; } bool MediaCodecReader::IsMediaSeekable() { // Check the MediaExtract flag if the source is seekable. - return (mExtractor != nullptr) && (mExtractor->flags() & MediaExtractor::CAN_SEEK); + return (mExtractor != nullptr) && + (mExtractor->flags() & MediaExtractor::CAN_SEEK); } -android::sp<android::MediaSource> +sp<MediaSource> MediaCodecReader::GetAudioOffloadTrack() { return mAudioOffloadTrack.mSource; } bool MediaCodecReader::ReallocateResources() { if (CreateLooper() && CreateExtractor() && CreateMediaSources() && - CreateMediaCodecs()) { + CreateMediaCodecs() && + CreateTaskQueues()) { return true; } ReleaseResources(); return false; } void MediaCodecReader::ReleaseCriticalResources() { ResetDecode(); // Before freeing a video codec, all video buffers needed to be released // even from graphics pipeline. VideoFrameContainer* videoframe = mDecoder->GetVideoFrameContainer(); - if (videoframe != nullptr) { + if (videoframe) { videoframe->ClearCurrentFrame(); } DestroyMediaCodecs(); ClearColorConverterBuffer(); } void MediaCodecReader::ReleaseResources() { ReleaseCriticalResources(); DestroyMediaSources(); DestroyExtractor(); DestroyLooper(); + ShutdownTaskQueues(); } bool MediaCodecReader::CreateLooper() { if (mLooper != nullptr) { return true; } @@ -657,17 +807,17 @@ MediaCodecReader::CreateMediaSources() const ssize_t invalidTrackIndex = -1; ssize_t audioTrackIndex = invalidTrackIndex; ssize_t videoTrackIndex = invalidTrackIndex; for (size_t i = 0; i < mExtractor->countTracks(); ++i) { sp<MetaData> trackFormat = mExtractor->getTrackMetaData(i); - const char *mime; + const char* mime; if (!trackFormat->findCString(kKeyMIMEType, &mime)) { continue; } if (audioTrackIndex == invalidTrackIndex && !strncasecmp(mime, "audio/", 6)) { audioTrackIndex = i; } else if (videoTrackIndex == invalidTrackIndex && @@ -694,49 +844,81 @@ MediaCodecReader::CreateMediaSources() if (videoTrackIndex != invalidTrackIndex && mVideoTrack.mSource == nullptr) { sp<MediaSource> videoSource = mExtractor->getTrack(videoTrackIndex); if (videoSource != nullptr && videoSource->start() == OK) { mVideoTrack.mSource = videoSource; } } return - (audioTrackIndex == invalidTrackIndex || mAudioTrack.mSource != nullptr) && - (videoTrackIndex == invalidTrackIndex || mVideoTrack.mSource != nullptr); + (audioTrackIndex == invalidTrackIndex || mAudioTrack.mSource != nullptr) && + (videoTrackIndex == invalidTrackIndex || mVideoTrack.mSource != nullptr); } void MediaCodecReader::DestroyMediaSources() { mAudioTrack.mSource = nullptr; mVideoTrack.mSource = nullptr; mAudioOffloadTrack.mSource = nullptr; } +void +MediaCodecReader::ShutdownTaskQueues() +{ + if(mAudioTrack.mTaskQueue) { + mAudioTrack.mTaskQueue->Shutdown(); + mAudioTrack.mTaskQueue = nullptr; + } + if(mVideoTrack.mTaskQueue) { + mVideoTrack.mTaskQueue->Shutdown(); + mVideoTrack.mTaskQueue = nullptr; + } +} + +bool +MediaCodecReader::CreateTaskQueues() +{ + if (mAudioTrack.mSource != nullptr && mAudioTrack.mCodec != nullptr && + !mAudioTrack.mTaskQueue) { + mAudioTrack.mTaskQueue = new MediaTaskQueue( + SharedThreadPool::Get(NS_LITERAL_CSTRING("MediaCodecReader Audio"), 1)); + NS_ENSURE_TRUE(mAudioTrack.mTaskQueue, false); + } + if (mVideoTrack.mSource != nullptr && mVideoTrack.mCodec != nullptr && + !mVideoTrack.mTaskQueue) { + mVideoTrack.mTaskQueue = new MediaTaskQueue( + SharedThreadPool::Get(NS_LITERAL_CSTRING("MediaCodecReader Video"), 1)); + NS_ENSURE_TRUE(mVideoTrack.mTaskQueue, false); + } + + return true; +} + bool MediaCodecReader::CreateMediaCodecs() { if (CreateMediaCodec(mLooper, mAudioTrack, false, nullptr) && CreateMediaCodec(mLooper, mVideoTrack, true, mVideoListener)) { return true; } return false; } bool -MediaCodecReader::CreateMediaCodec(sp<ALooper> &aLooper, - Track &aTrack, +MediaCodecReader::CreateMediaCodec(sp<ALooper>& aLooper, + Track& aTrack, bool aAsync, wp<MediaCodecProxy::CodecResourceListener> aListener) { if (aTrack.mSource != nullptr && aTrack.mCodec == nullptr) { sp<MetaData> sourceFormat = aTrack.mSource->getFormat(); - const char *mime; + const char* mime; if (sourceFormat->findCString(kKeyMIMEType, &mime)) { aTrack.mCodec = MediaCodecProxy::CreateByType(aLooper, mime, false, aAsync, aListener); } if (aTrack.mCodec == nullptr) { NS_WARNING("Couldn't create MediaCodecProxy"); return false; } @@ -757,17 +939,17 @@ MediaCodecReader::CreateMediaCodec(sp<AL } } } return true; } bool -MediaCodecReader::ConfigureMediaCodec(Track &aTrack) +MediaCodecReader::ConfigureMediaCodec(Track& aTrack) { if (aTrack.mSource != nullptr && aTrack.mCodec != nullptr) { if (!aTrack.mCodec->allocated()) { return false; } sp<MetaData> sourceFormat = aTrack.mSource->getFormat(); @@ -803,17 +985,17 @@ MediaCodecReader::ConfigureMediaCodec(Tr void MediaCodecReader::DestroyMediaCodecs() { DestroyMediaCodecs(mAudioTrack); DestroyMediaCodecs(mVideoTrack); } void -MediaCodecReader::DestroyMediaCodecs(Track &aTrack) +MediaCodecReader::DestroyMediaCodecs(Track& aTrack) { aTrack.mCodec = nullptr; } bool MediaCodecReader::UpdateDuration() { // read audio duration @@ -847,17 +1029,18 @@ MediaCodecReader::UpdateDuration() bool MediaCodecReader::UpdateAudioInfo() { if (mAudioTrack.mSource == nullptr && mAudioTrack.mCodec == nullptr) { // No needs to update AudioInfo if no audio streams. return true; } - if (mAudioTrack.mSource == nullptr || mAudioTrack.mCodec == nullptr || !mAudioTrack.mCodec->allocated()) { + if (mAudioTrack.mSource == nullptr || mAudioTrack.mCodec == nullptr || + !mAudioTrack.mCodec->allocated()) { // Something wrong. MOZ_ASSERT(mAudioTrack.mSource != nullptr, "mAudioTrack.mSource should not be nullptr"); MOZ_ASSERT(mAudioTrack.mCodec != nullptr, "mAudioTrack.mCodec should not be nullptr"); MOZ_ASSERT(mAudioTrack.mCodec->allocated(), "mAudioTrack.mCodec->allocated() should not be false"); return false; } // read audio metadata from MediaSource @@ -868,17 +1051,18 @@ MediaCodecReader::UpdateAudioInfo() // ensure audio metadata from MediaCodec has been parsed if (!EnsureCodecFormatParsed(mAudioTrack)){ return false; } // read audio metadata from MediaCodec sp<AMessage> audioCodecFormat; - if (mAudioTrack.mCodec->getOutputFormat(&audioCodecFormat) != OK || audioCodecFormat == nullptr) { + if (mAudioTrack.mCodec->getOutputFormat(&audioCodecFormat) != OK || + audioCodecFormat == nullptr) { return false; } AString codec_mime; int32_t codec_channel_count = 0; int32_t codec_sample_rate = 0; if (!audioCodecFormat->findString("mime", &codec_mime) || !audioCodecFormat->findInt32("channel-count", &codec_channel_count) || @@ -897,17 +1081,18 @@ MediaCodecReader::UpdateAudioInfo() bool MediaCodecReader::UpdateVideoInfo() { if (mVideoTrack.mSource == nullptr && mVideoTrack.mCodec == nullptr) { // No needs to update VideoInfo if no video streams. return true; } - if (mVideoTrack.mSource == nullptr || mVideoTrack.mCodec == nullptr || !mVideoTrack.mCodec->allocated()) { + if (mVideoTrack.mSource == nullptr || mVideoTrack.mCodec == nullptr || + !mVideoTrack.mCodec->allocated()) { // Something wrong. MOZ_ASSERT(mVideoTrack.mSource != nullptr, "mVideoTrack.mSource should not be nullptr"); MOZ_ASSERT(mVideoTrack.mCodec != nullptr, "mVideoTrack.mCodec should not be nullptr"); MOZ_ASSERT(mVideoTrack.mCodec->allocated(), "mVideoTrack.mCodec->allocated() should not be false"); return false; } // read video metadata from MediaSource @@ -929,17 +1114,18 @@ MediaCodecReader::UpdateVideoInfo() // ensure video metadata from MediaCodec has been parsed if (!EnsureCodecFormatParsed(mVideoTrack)){ return false; } // read video metadata from MediaCodec sp<AMessage> videoCodecFormat; - if (mVideoTrack.mCodec->getOutputFormat(&videoCodecFormat) != OK || videoCodecFormat == nullptr) { + if (mVideoTrack.mCodec->getOutputFormat(&videoCodecFormat) != OK || + videoCodecFormat == nullptr) { return false; } AString codec_mime; int32_t codec_width = 0; int32_t codec_height = 0; int32_t codec_stride = 0; int32_t codec_slice_height = 0; int32_t codec_color_format = 0; @@ -948,17 +1134,18 @@ MediaCodecReader::UpdateVideoInfo() int32_t codec_crop_right = 0; int32_t codec_crop_bottom = 0; if (!videoCodecFormat->findString("mime", &codec_mime) || !videoCodecFormat->findInt32("width", &codec_width) || !videoCodecFormat->findInt32("height", &codec_height) || !videoCodecFormat->findInt32("stride", &codec_stride) || !videoCodecFormat->findInt32("slice-height", &codec_slice_height) || !videoCodecFormat->findInt32("color-format", &codec_color_format) || - !videoCodecFormat->findRect("crop", &codec_crop_left, &codec_crop_top, &codec_crop_right, &codec_crop_bottom)) { + !videoCodecFormat->findRect("crop", &codec_crop_left, &codec_crop_top, + &codec_crop_right, &codec_crop_bottom)) { return false; } mVideoTrack.mWidth = codec_width; mVideoTrack.mHeight = codec_height; mVideoTrack.mStride = codec_stride; mVideoTrack.mSliceHeight = codec_slice_height; mVideoTrack.mColorFormat = codec_color_format; @@ -975,89 +1162,96 @@ MediaCodecReader::UpdateVideoInfo() // Relative picture size gfx::IntRect relative_picture_rect = gfx::ToIntRect(picture_rect); if (mVideoTrack.mWidth != mVideoTrack.mFrameSize.width || mVideoTrack.mHeight != mVideoTrack.mFrameSize.height) { // Frame size is different from what the container reports. This is legal, // and we will preserve the ratio of the crop rectangle as it // was reported relative to the picture size reported by the container. - relative_picture_rect.x = (picture_rect.x * mVideoTrack.mWidth) / mVideoTrack.mFrameSize.width; - relative_picture_rect.y = (picture_rect.y * mVideoTrack.mHeight) / mVideoTrack.mFrameSize.height; - relative_picture_rect.width = (picture_rect.width * mVideoTrack.mWidth) / mVideoTrack.mFrameSize.width; - relative_picture_rect.height = (picture_rect.height * mVideoTrack.mHeight) / mVideoTrack.mFrameSize.height; + relative_picture_rect.x = (picture_rect.x * mVideoTrack.mWidth) / + mVideoTrack.mFrameSize.width; + relative_picture_rect.y = (picture_rect.y * mVideoTrack.mHeight) / + mVideoTrack.mFrameSize.height; + relative_picture_rect.width = (picture_rect.width * mVideoTrack.mWidth) / + mVideoTrack.mFrameSize.width; + relative_picture_rect.height = (picture_rect.height * mVideoTrack.mHeight) / + mVideoTrack.mFrameSize.height; } // Update VideoInfo mInfo.mVideo.mHasVideo = true; mVideoTrack.mPictureRect = picture_rect; mInfo.mVideo.mDisplay = display_size; mVideoTrack.mRelativePictureRect = relative_picture_rect; return true; } status_t -MediaCodecReader::FlushCodecData(Track &aTrack) +MediaCodecReader::FlushCodecData(Track& aTrack) { - if (aTrack.mSource == nullptr || aTrack.mCodec == nullptr || !aTrack.mCodec->allocated()) { + if (aTrack.mSource == nullptr || aTrack.mCodec == nullptr || + !aTrack.mCodec->allocated()) { return UNKNOWN_ERROR; } status_t status = aTrack.mCodec->flush(); aTrack.mFlushed = (status == OK); if (aTrack.mFlushed) { aTrack.mInputIndex = sInvalidInputIndex; } return status; } // Keep filling data if there are available buffers. // FIXME: change to non-blocking read status_t -MediaCodecReader::FillCodecInputData(Track &aTrack) +MediaCodecReader::FillCodecInputData(Track& aTrack) { - if (aTrack.mSource == nullptr || aTrack.mCodec == nullptr || !aTrack.mCodec->allocated()) { + if (aTrack.mSource == nullptr || aTrack.mCodec == nullptr || + !aTrack.mCodec->allocated()) { return UNKNOWN_ERROR; } if (aTrack.mInputEndOfStream) { return ERROR_END_OF_STREAM; } if (IsValidTimestampUs(aTrack.mSeekTimeUs) && !aTrack.mFlushed) { FlushCodecData(aTrack); } size_t index = 0; - while (aTrack.mInputIndex.isValid() || aTrack.mCodec->dequeueInputBuffer(&index) == OK) { + while (aTrack.mInputIndex.isValid() || + aTrack.mCodec->dequeueInputBuffer(&index) == OK) { if (!aTrack.mInputIndex.isValid()) { aTrack.mInputIndex = index; } MOZ_ASSERT(aTrack.mInputIndex.isValid(), "aElement.mInputIndex should be valid"); - MediaBuffer *source_buffer = nullptr; + MediaBuffer* source_buffer = nullptr; status_t status = OK; if (IsValidTimestampUs(aTrack.mSeekTimeUs)) { MediaSource::ReadOptions options; options.setSeekTo(aTrack.mSeekTimeUs); status = aTrack.mSource->read(&source_buffer, &options); } else { status = aTrack.mSource->read(&source_buffer); } // read() fails if (status == INFO_FORMAT_CHANGED) { return INFO_FORMAT_CHANGED; } else if (status == ERROR_END_OF_STREAM) { aTrack.mInputEndOfStream = true; - status = aTrack.mCodec->queueInputBuffer(aTrack.mInputIndex.value(), - 0, 0, 0, - MediaCodec::BUFFER_FLAG_EOS); + aTrack.mCodec->queueInputBuffer(aTrack.mInputIndex.value(), + 0, 0, 0, + MediaCodec::BUFFER_FLAG_EOS); return ERROR_END_OF_STREAM; } else if (status == -ETIMEDOUT) { return OK; // try it later } else if (status != OK) { return status; } else if (source_buffer == nullptr) { return UNKNOWN_ERROR; } @@ -1075,62 +1269,58 @@ MediaCodecReader::FillCodecInputData(Tra aTrack.mInputCopier->Copy(source_buffer, input_buffer)) { int64_t timestamp = sInvalidTimestampUs; sp<MetaData> codec_format = source_buffer->meta_data(); if (codec_format != nullptr) { codec_format->findInt64(kKeyTime, ×tamp); } status = aTrack.mCodec->queueInputBuffer( - aTrack.mInputIndex.value(), input_buffer->offset(), input_buffer->size(), timestamp, 0); + aTrack.mInputIndex.value(), input_buffer->offset(), + input_buffer->size(), timestamp, 0); if (status == OK) { aTrack.mInputIndex = sInvalidInputIndex; } } source_buffer->release(); if (status != OK) { return status; } } return OK; } status_t -MediaCodecReader::GetCodecOutputData(Track &aTrack, - CodecBufferInfo &aBuffer, +MediaCodecReader::GetCodecOutputData(Track& aTrack, + CodecBufferInfo& aBuffer, int64_t aThreshold, - const TimeStamp &aTimeout) + const TimeStamp& aTimeout) { // Read next frame. CodecBufferInfo info; - - // Try to fill more input buffers and then get one output buffer. - // FIXME: use callback from MediaCodec status_t status = OK; + while (status == OK || status == INFO_OUTPUT_BUFFERS_CHANGED || + status == -EAGAIN) { - while (status == OK || status == INFO_OUTPUT_BUFFERS_CHANGED || - status == -EAGAIN || status == ERROR_END_OF_STREAM) { - // Try to fill more input buffers and then get one output buffer. - // FIXME: use callback from MediaCodec - status = FillCodecInputData(aTrack); int64_t duration = (int64_t)(aTimeout - TimeStamp::Now()).ToMicroseconds(); if (!IsValidDurationUs(duration)) { return -EAGAIN; } - if (status == OK || status == ERROR_END_OF_STREAM) { - status = aTrack.mCodec->dequeueOutputBuffer( - &info.mIndex, &info.mOffset, &info.mSize, &info.mTimeUs, &info.mFlags, duration); - if (info.mFlags & MediaCodec::BUFFER_FLAG_EOS) { - aBuffer = info; - aBuffer.mBuffer = aTrack.mOutputBuffers[info.mIndex]; - return ERROR_END_OF_STREAM; - } + status = aTrack.mCodec->dequeueOutputBuffer(&info.mIndex, &info.mOffset, + &info.mSize, &info.mTimeUs, &info.mFlags, duration); + // Check EOS first. + if (status == ERROR_END_OF_STREAM || + info.mFlags & MediaCodec::BUFFER_FLAG_EOS) { + aBuffer = info; + aBuffer.mBuffer = aTrack.mOutputBuffers[info.mIndex]; + aTrack.mOutputEndOfStream = true; + return ERROR_END_OF_STREAM; } if (status == OK) { if (!IsValidTimestampUs(aThreshold) || info.mTimeUs >= aThreshold) { // Get a valid output buffer. break; } else { aTrack.mCodec->releaseOutputBuffer(info.mIndex); @@ -1163,48 +1353,50 @@ MediaCodecReader::GetCodecOutputData(Tra aBuffer = info; aBuffer.mBuffer = aTrack.mOutputBuffers[info.mIndex]; return OK; } bool -MediaCodecReader::EnsureCodecFormatParsed(Track &aTrack) +MediaCodecReader::EnsureCodecFormatParsed(Track& aTrack) { - if (aTrack.mSource == nullptr || aTrack.mCodec == nullptr || !aTrack.mCodec->allocated()) { + if (aTrack.mSource == nullptr || aTrack.mCodec == nullptr || + !aTrack.mCodec->allocated()) { return false; } sp<AMessage> format; if (aTrack.mCodec->getOutputFormat(&format) == OK) { return true; } status_t status = OK; size_t index = 0; size_t offset = 0; size_t size = 0; int64_t timeUs = 0LL; uint32_t flags = 0; - while ((status = aTrack.mCodec->dequeueOutputBuffer(&index, &offset, &size, &timeUs, &flags)) != INFO_FORMAT_CHANGED) { + while ((status = aTrack.mCodec->dequeueOutputBuffer(&index, &offset, &size, + &timeUs, &flags)) != INFO_FORMAT_CHANGED) { if (status == OK) { aTrack.mCodec->releaseOutputBuffer(index); } status = FillCodecInputData(aTrack); if (status == INFO_FORMAT_CHANGED) { break; } else if (status != OK) { return false; } } return aTrack.mCodec->getOutputFormat(&format) == OK; } -uint8_t * +uint8_t* MediaCodecReader::GetColorConverterBuffer(int32_t aWidth, int32_t aHeight) { // Allocate a temporary YUV420Planer buffer. size_t yuv420p_y_size = aWidth * aHeight; size_t yuv420p_u_size = ((aWidth + 1) / 2) * ((aHeight + 1) / 2); size_t yuv420p_v_size = yuv420p_u_size; size_t yuv420p_size = yuv420p_y_size + yuv420p_u_size + yuv420p_v_size; if (mColorConverterBufferSize != yuv420p_size) { @@ -1219,17 +1411,17 @@ void MediaCodecReader::ClearColorConverterBuffer() { mColorConverterBuffer = nullptr; mColorConverterBufferSize = 0; } // Called on MediaCodecReader::mLooper thread. void -MediaCodecReader::onMessageReceived(const sp<AMessage> &aMessage) +MediaCodecReader::onMessageReceived(const sp<AMessage>& aMessage) { switch (aMessage->what()) { case kNotifyCodecReserved: { // Our decode may have acquired the hardware resource that it needs // to start. Notify the state machine to resume loading metadata. mDecoder->NotifyWaitingForResourcesStatusChanged();
--- a/content/media/omx/MediaCodecReader.h +++ b/content/media/omx/MediaCodecReader.h @@ -24,16 +24,18 @@ struct AMessage; class MOZ_EXPORT MediaExtractor; class MOZ_EXPORT MediaBuffer; struct MOZ_EXPORT MediaSource; struct MediaCodec; } // namespace android namespace mozilla { +class MediaTaskQueue; + class MediaCodecReader : public MediaOmxCommonReader { public: MediaCodecReader(AbstractMediaDecoder* aDecoder); virtual ~MediaCodecReader(); // Initializes the reader, returns NS_OK on success, or NS_ERROR_FAILURE // on failure. @@ -48,27 +50,25 @@ public: // Release media resources they should be released in dormant state virtual void ReleaseMediaResources(); // Destroys the decoding state. The reader cannot be made usable again. // This is different from ReleaseMediaResources() as Shutdown() is // irreversible, whereas ReleaseMediaResources() is reversible. virtual void Shutdown(); - // Decodes an unspecified amount of audio data, enqueuing the audio data - // in mAudioQueue. Returns true when there's more audio to decode, - // false if the audio is finished, end of file has been reached, - // or an un-recoverable read error has occured. - virtual bool DecodeAudioData(); + // Flush the MediaTaskQueue, flush MediaCodec and raise the mDiscontinuity. + virtual nsresult ResetDecode() MOZ_OVERRIDE; - // Reads and decodes one video frame. Packets with a timestamp less - // than aTimeThreshold will be decoded (unless they're not keyframes - // and aKeyframeSkip is true), but will not be added to the queue. - virtual bool DecodeVideoFrame(bool &aKeyframeSkip, - int64_t aTimeThreshold); + // Disptach a DecodeVideoFrameTask to decode video data. + virtual void RequestVideoData(bool aSkipToNextKeyframe, + int64_t aTimeThreshold) MOZ_OVERRIDE; + + // Disptach a DecodeAduioDataTask to decode video data. + virtual void RequestAudioData() MOZ_OVERRIDE; virtual bool HasAudio(); virtual bool HasVideo(); // Read header data for all bitstreams in the file. Fills aInfo with // the data required to present the media, and optionally fills *aTags // with tag metadata from the file. // Returns NS_OK on success, or NS_ERROR_FAILURE on failure. @@ -85,17 +85,18 @@ public: virtual bool IsMediaSeekable() MOZ_OVERRIDE; virtual android::sp<android::MediaSource> GetAudioOffloadTrack(); protected: struct TrackInputCopier { - virtual bool Copy(android::MediaBuffer* aSourceBuffer, android::sp<android::ABuffer> aCodecBuffer); + virtual bool Copy(android::MediaBuffer* aSourceBuffer, + android::sp<android::ABuffer> aCodecBuffer); }; struct Track { Track(); // pipeline parameters android::sp<android::MediaSource> mSource; @@ -106,75 +107,82 @@ protected: // pipeline copier nsAutoPtr<TrackInputCopier> mInputCopier; // media parameters int64_t mDurationUs; // playback parameters CheckedUint32 mInputIndex; + // mDiscontinuity, mFlushed, mInputEndOfStream, mInputEndOfStream, + // mSeekTimeUs don't be protected by a lock because the + // mTaskQueue->Flush() will flush all tasks. bool mInputEndOfStream; + bool mOutputEndOfStream; int64_t mSeekTimeUs; bool mFlushed; // meaningless when mSeekTimeUs is invalid. + bool mDiscontinuity; + nsRefPtr<MediaTaskQueue> mTaskQueue; }; // Receive a message from MessageHandler. // Called on MediaCodecReader::mLooper thread. - void onMessageReceived(const android::sp<android::AMessage> &aMessage); + void onMessageReceived(const android::sp<android::AMessage>& aMessage); // Receive a notify from ResourceListener. // Called on Binder thread. virtual void codecReserved(Track& aTrack); virtual void codecCanceled(Track& aTrack); private: // An intermediary class that can be managed by android::sp<T>. // Redirect onMessageReceived() to MediaCodecReader. class MessageHandler : public android::AHandler { public: - MessageHandler(MediaCodecReader *aReader); + MessageHandler(MediaCodecReader* aReader); ~MessageHandler(); - virtual void onMessageReceived(const android::sp<android::AMessage> &aMessage); + virtual void onMessageReceived(const android::sp<android::AMessage>& aMessage); private: // Forbidden MessageHandler() MOZ_DELETE; - MessageHandler(const MessageHandler &rhs) MOZ_DELETE; - const MessageHandler &operator=(const MessageHandler &rhs) MOZ_DELETE; + MessageHandler(const MessageHandler& rhs) MOZ_DELETE; + const MessageHandler& operator=(const MessageHandler& rhs) MOZ_DELETE; MediaCodecReader *mReader; }; friend class MessageHandler; // An intermediary class that can be managed by android::sp<T>. // Redirect codecReserved() and codecCanceled() to MediaCodecReader. class VideoResourceListener : public android::MediaCodecProxy::CodecResourceListener { public: - VideoResourceListener(MediaCodecReader *aReader); + VideoResourceListener(MediaCodecReader* aReader); ~VideoResourceListener(); virtual void codecReserved(); virtual void codecCanceled(); private: // Forbidden VideoResourceListener() MOZ_DELETE; - VideoResourceListener(const VideoResourceListener &rhs) MOZ_DELETE; - const VideoResourceListener &operator=(const VideoResourceListener &rhs) MOZ_DELETE; + VideoResourceListener(const VideoResourceListener& rhs) MOZ_DELETE; + const VideoResourceListener& operator=(const VideoResourceListener& rhs) MOZ_DELETE; - MediaCodecReader *mReader; + MediaCodecReader* mReader; }; friend class VideoResourceListener; class VorbisInputCopier : public TrackInputCopier { - virtual bool Copy(android::MediaBuffer* aSourceBuffer, android::sp<android::ABuffer> aCodecBuffer); + virtual bool Copy(android::MediaBuffer* aSourceBuffer, + android::sp<android::ABuffer> aCodecBuffer); }; struct AudioTrack : public Track { AudioTrack(); }; struct VideoTrack : public Track @@ -201,53 +209,71 @@ private: size_t mOffset; size_t mSize; int64_t mTimeUs; uint32_t mFlags; }; // Forbidden MediaCodecReader() MOZ_DELETE; - const MediaCodecReader &operator=(const MediaCodecReader &rhs) MOZ_DELETE; + const MediaCodecReader& operator=(const MediaCodecReader& rhs) MOZ_DELETE; bool ReallocateResources(); void ReleaseCriticalResources(); void ReleaseResources(); bool CreateLooper(); void DestroyLooper(); bool CreateExtractor(); void DestroyExtractor(); bool CreateMediaSources(); void DestroyMediaSources(); bool CreateMediaCodecs(); - static bool CreateMediaCodec(android::sp<android::ALooper> &aLooper, - Track &aTrack, + static bool CreateMediaCodec(android::sp<android::ALooper>& aLooper, + Track& aTrack, bool aAsync, android::wp<android::MediaCodecProxy::CodecResourceListener> aListener); - static bool ConfigureMediaCodec(Track &aTrack); + static bool ConfigureMediaCodec(Track& aTrack); void DestroyMediaCodecs(); - static void DestroyMediaCodecs(Track &aTrack); + static void DestroyMediaCodecs(Track& aTrack); + + bool CreateTaskQueues(); + void ShutdownTaskQueues(); + bool DecodeVideoFrameTask(int64_t aTimeThreshold); + bool DecodeVideoFrameSync(int64_t aTimeThreshold); + bool DecodeAudioDataTask(); + bool DecodeAudioDataSync(); + void DispatchVideoTask(int64_t aTimeThreshold); + void DispatchAudioTask(); + inline bool CheckVideoResources() { + return (HasVideo() && mVideoTrack.mSource != nullptr && + mVideoTrack.mTaskQueue); + } + + inline bool CheckAudioResources() { + return (HasAudio() && mAudioTrack.mSource != nullptr && + mAudioTrack.mTaskQueue); + } bool UpdateDuration(); bool UpdateAudioInfo(); bool UpdateVideoInfo(); - static android::status_t FlushCodecData(Track &aTrack); - static android::status_t FillCodecInputData(Track &aTrack); - static android::status_t GetCodecOutputData(Track &aTrack, - CodecBufferInfo &aBuffer, + static android::status_t FlushCodecData(Track& aTrack); + static android::status_t FillCodecInputData(Track& aTrack); + static android::status_t GetCodecOutputData(Track& aTrack, + CodecBufferInfo& aBuffer, int64_t aThreshold, - const TimeStamp &aTimeout); - static bool EnsureCodecFormatParsed(Track &aTrack); + const TimeStamp& aTimeout); + static bool EnsureCodecFormatParsed(Track& aTrack); - uint8_t *GetColorConverterBuffer(int32_t aWidth, int32_t aHeight); + uint8_t* GetColorConverterBuffer(int32_t aWidth, int32_t aHeight); void ClearColorConverterBuffer(); android::sp<MessageHandler> mHandler; android::sp<VideoResourceListener> mVideoListener; android::sp<android::ALooper> mLooper; android::sp<android::MediaExtractor> mExtractor;
--- a/content/xul/document/src/XULDocument.cpp +++ b/content/xul/document/src/XULDocument.cpp @@ -28,32 +28,28 @@ #include "nsError.h" #include "nsIBoxObject.h" #include "nsIChromeRegistry.h" #include "nsView.h" #include "nsViewManager.h" #include "nsIContentViewer.h" #include "nsIDOMXULElement.h" -#include "nsIRDFNode.h" -#include "nsIRDFRemoteDataSource.h" -#include "nsIRDFService.h" #include "nsIStreamListener.h" #include "nsITimer.h" #include "nsDocShell.h" #include "nsGkAtoms.h" #include "nsXMLContentSink.h" #include "nsXULContentSink.h" #include "nsXULContentUtils.h" #include "nsIXULOverlayProvider.h" +#include "nsIStringEnumerator.h" #include "nsNetUtil.h" #include "nsParserCIID.h" #include "nsPIBoxObject.h" -#include "nsRDFCID.h" -#include "nsILocalStore.h" #include "nsXPIDLString.h" #include "nsPIDOMWindow.h" #include "nsPIWindowRoot.h" #include "nsXULCommandDispatcher.h" #include "nsXULElement.h" #include "prlog.h" #include "rdf.h" #include "nsIFrame.h" @@ -132,21 +128,16 @@ const uint32_t kMaxAttributeLength = 409 //---------------------------------------------------------------------- // // Statics // int32_t XULDocument::gRefCnt = 0; -nsIRDFService* XULDocument::gRDFService; -nsIRDFResource* XULDocument::kNC_persist; -nsIRDFResource* XULDocument::kNC_attribute; -nsIRDFResource* XULDocument::kNC_value; - PRLogModuleInfo* XULDocument::gXULLog; //---------------------------------------------------------------------- struct BroadcasterMapEntry : public PLDHashEntryHdr { Element* mBroadcaster; // [WEAK] nsSmallVoidArray mListeners; // [OWNING] of BroadcastListener objects }; @@ -225,36 +216,21 @@ XULDocument::~XULDocument() // look for persisted data: mPersistenceIds.Clear(); // Destroy our broadcaster map. if (mBroadcasterMap) { PL_DHashTableDestroy(mBroadcasterMap); } - if (mLocalStore) { - nsCOMPtr<nsIRDFRemoteDataSource> remote = - do_QueryInterface(mLocalStore); - if (remote) - remote->Flush(); - } - delete mTemplateBuilderTable; Preferences::UnregisterCallback(XULDocument::DirectionChanged, "intl.uidirection.", this); - if (--gRefCnt == 0) { - NS_IF_RELEASE(gRDFService); - - NS_IF_RELEASE(kNC_persist); - NS_IF_RELEASE(kNC_attribute); - NS_IF_RELEASE(kNC_value); - } - if (mOffThreadCompileStringBuf) { js_free(mOffThreadCompileStringBuf); } } } // namespace dom } // namespace mozilla @@ -344,16 +320,17 @@ NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_ } NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(XULDocument, XMLDocument) delete tmp->mTemplateBuilderTable; tmp->mTemplateBuilderTable = nullptr; NS_IMPL_CYCLE_COLLECTION_UNLINK(mCommandDispatcher) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mLocalStore) //XXX We should probably unlink all the objects we traverse. NS_IMPL_CYCLE_COLLECTION_UNLINK_END NS_IMPL_ADDREF_INHERITED(XULDocument, XMLDocument) NS_IMPL_RELEASE_INHERITED(XULDocument, XMLDocument) // QueryInterface implementation for XULDocument @@ -1326,126 +1303,60 @@ XULDocument::Persist(const nsAString& aI } tag = do_GetAtom(aAttr); NS_ENSURE_TRUE(tag, NS_ERROR_OUT_OF_MEMORY); nameSpaceID = kNameSpaceID_None; } - rv = Persist(element, nameSpaceID, tag); - if (NS_FAILED(rv)) return rv; - - return NS_OK; + return Persist(element, nameSpaceID, tag); } nsresult XULDocument::Persist(nsIContent* aElement, int32_t aNameSpaceID, nsIAtom* aAttribute) { // For non-chrome documents, persistance is simply broken if (!nsContentUtils::IsSystemPrincipal(NodePrincipal())) return NS_ERROR_NOT_AVAILABLE; - // First make sure we _have_ a local store to stuff the persisted - // information into. (We might not have one if profile information - // hasn't been loaded yet...) - if (!mLocalStore) - return NS_OK; - - nsresult rv; - - nsCOMPtr<nsIRDFResource> element; - rv = nsXULContentUtils::GetElementResource(aElement, getter_AddRefs(element)); - if (NS_FAILED(rv)) return rv; - - // No ID, so nothing to persist. - if (! element) - return NS_OK; - - // Ick. Construct a property from the attribute. Punt on - // namespaces for now. - // Don't bother with unreasonable attributes. We clamp long values, - // but truncating attribute names turns it into a different attribute - // so there's no point in persisting anything at all - nsAtomCString attrstr(aAttribute); - if (attrstr.Length() > kMaxAttrNameLength) { - NS_WARNING("Can't persist, Attribute name too long"); - return NS_ERROR_ILLEGAL_VALUE; + if (!mLocalStore) { + mLocalStore = do_GetService("@mozilla.org/xul/xulstore;1"); + if (NS_WARN_IF(!mLocalStore)) { + return NS_ERROR_NOT_INITIALIZED; + } } - nsCOMPtr<nsIRDFResource> attr; - rv = gRDFService->GetResource(attrstr, - getter_AddRefs(attr)); - if (NS_FAILED(rv)) return rv; - - // Turn the value into a literal + nsAutoString id; + + aElement->GetAttr(kNameSpaceID_None, nsGkAtoms::id, id); + nsAtomString attrstr(aAttribute); + nsAutoString valuestr; aElement->GetAttr(kNameSpaceID_None, aAttribute, valuestr); - // prevent over-long attributes that choke the parser (bug 319846) - // (can't simply Truncate without testing, it's implemented - // using SetLength and will grow a short string) - if (valuestr.Length() > kMaxAttributeLength) { - NS_WARNING("Truncating persisted attribute value"); - valuestr.Truncate(kMaxAttributeLength); - } - - // See if there was an old value... - nsCOMPtr<nsIRDFNode> oldvalue; - rv = mLocalStore->GetTarget(element, attr, true, getter_AddRefs(oldvalue)); - if (NS_FAILED(rv)) return rv; - - if (oldvalue && valuestr.IsEmpty()) { - // ...there was an oldvalue, and they've removed it. XXXThis - // handling isn't quite right... - rv = mLocalStore->Unassert(element, attr, oldvalue); + nsAutoCString utf8uri; + nsresult rv = mDocumentURI->GetSpec(utf8uri); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; } - else { - // Now either 'change' or 'assert' based on whether there was - // an old value. - nsCOMPtr<nsIRDFLiteral> newvalue; - rv = gRDFService->GetLiteral(valuestr.get(), getter_AddRefs(newvalue)); - if (NS_FAILED(rv)) return rv; - - if (oldvalue) { - if (oldvalue != newvalue) - rv = mLocalStore->Change(element, attr, oldvalue, newvalue); - else - rv = NS_OK; - } - else { - rv = mLocalStore->Assert(element, attr, newvalue, true); - } + NS_ConvertUTF8toUTF16 uri(utf8uri); + + bool hasAttr; + rv = mLocalStore->HasValue(uri, id, attrstr, &hasAttr); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; } - if (NS_FAILED(rv)) return rv; - - // Add it to the persisted set for this document (if it's not - // there already). - { - nsAutoCString docurl; - rv = mDocumentURI->GetSpec(docurl); - if (NS_FAILED(rv)) return rv; - - nsCOMPtr<nsIRDFResource> doc; - rv = gRDFService->GetResource(docurl, getter_AddRefs(doc)); - if (NS_FAILED(rv)) return rv; - - bool hasAssertion; - rv = mLocalStore->HasAssertion(doc, kNC_persist, element, true, &hasAssertion); - if (NS_FAILED(rv)) return rv; - - if (! hasAssertion) { - rv = mLocalStore->Assert(doc, kNC_persist, element, true); - if (NS_FAILED(rv)) return rv; - } + if (hasAttr && valuestr.IsEmpty()) { + return mLocalStore->RemoveValue(uri, id, attrstr); + } else { + return mLocalStore->SetValue(uri, id, attrstr, valuestr); } - - return NS_OK; } nsresult XULDocument::GetViewportSize(int32_t* aWidth, int32_t* aHeight) { *aWidth = *aHeight = 0; @@ -1988,35 +1899,17 @@ XULDocument::Init() { nsresult rv = XMLDocument::Init(); NS_ENSURE_SUCCESS(rv, rv); // Create our command dispatcher and hook it up. mCommandDispatcher = new nsXULCommandDispatcher(this); NS_ENSURE_TRUE(mCommandDispatcher, NS_ERROR_OUT_OF_MEMORY); - // this _could_ fail; e.g., if we've tried to grab the local store - // before profiles have initialized. If so, no big deal; nothing - // will persist. - mLocalStore = do_GetService(NS_LOCALSTORE_CONTRACTID); - if (gRefCnt++ == 0) { - // Keep the RDF service cached in a member variable to make using - // it a bit less painful - rv = CallGetService("@mozilla.org/rdf/rdf-service;1", &gRDFService); - NS_ASSERTION(NS_SUCCEEDED(rv), "unable to get RDF Service"); - if (NS_FAILED(rv)) return rv; - - gRDFService->GetResource(NS_LITERAL_CSTRING(NC_NAMESPACE_URI "persist"), - &kNC_persist); - gRDFService->GetResource(NS_LITERAL_CSTRING(NC_NAMESPACE_URI "attribute"), - &kNC_attribute); - gRDFService->GetResource(NS_LITERAL_CSTRING(NC_NAMESPACE_URI "value"), - &kNC_value); - // ensure that the XUL prototype cache is instantiated successfully, // so that we can use nsXULPrototypeCache::GetInstance() without // null-checks in the rest of the class. nsXULPrototypeCache* cache = nsXULPrototypeCache::GetInstance(); if (!cache) { NS_ERROR("Could not instantiate nsXULPrototypeCache"); return NS_ERROR_FAILURE; } @@ -2170,154 +2063,133 @@ nsresult XULDocument::ApplyPersistentAttributes() { // For non-chrome documents, persistance is simply broken if (!nsContentUtils::IsSystemPrincipal(NodePrincipal())) return NS_ERROR_NOT_AVAILABLE; // Add all of the 'persisted' attributes into the content // model. - if (!mLocalStore) - return NS_OK; + if (!mLocalStore) { + mLocalStore = do_GetService("@mozilla.org/xul/xulstore;1"); + if (NS_WARN_IF(!mLocalStore)) { + return NS_ERROR_NOT_INITIALIZED; + } + } mApplyingPersistedAttrs = true; ApplyPersistentAttributesInternal(); mApplyingPersistedAttrs = false; // After we've applied persistence once, we should only reapply // it to nodes created by overlays mRestrictPersistence = true; mPersistenceIds.Clear(); return NS_OK; } -nsresult +nsresult XULDocument::ApplyPersistentAttributesInternal() { nsCOMArray<nsIContent> elements; - nsAutoCString docurl; - mDocumentURI->GetSpec(docurl); - - nsCOMPtr<nsIRDFResource> doc; - gRDFService->GetResource(docurl, getter_AddRefs(doc)); - - nsCOMPtr<nsISimpleEnumerator> persisted; - mLocalStore->GetTargets(doc, kNC_persist, true, getter_AddRefs(persisted)); + nsAutoCString utf8uri; + nsresult rv = mDocumentURI->GetSpec(utf8uri); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + NS_ConvertUTF8toUTF16 uri(utf8uri); + + // Get a list of element IDs for which persisted values are available + nsCOMPtr<nsIStringEnumerator> ids; + rv = mLocalStore->GetIDsEnumerator(uri, getter_AddRefs(ids)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } while (1) { bool hasmore = false; - persisted->HasMoreElements(&hasmore); - if (! hasmore) + ids->HasMore(&hasmore); + if (!hasmore) { break; - - nsCOMPtr<nsISupports> isupports; - persisted->GetNext(getter_AddRefs(isupports)); - - nsCOMPtr<nsIRDFResource> resource = do_QueryInterface(isupports); - if (! resource) { - NS_WARNING("expected element to be a resource"); + } + + nsAutoString id; + ids->GetNext(id); + + if (mRestrictPersistence && !mPersistenceIds.Contains(id)) { continue; } - const char *uri; - resource->GetValueConst(&uri); - if (! uri) - continue; - - nsAutoString id; - nsXULContentUtils::MakeElementID(this, nsDependentCString(uri), id); - - if (id.IsEmpty()) - continue; - - if (mRestrictPersistence && !mPersistenceIds.Contains(id)) - continue; - // This will clear the array if there are no elements. GetElementsForID(id, elements); - - if (!elements.Count()) + if (!elements.Count()) { continue; - - ApplyPersistentAttributesToElements(resource, elements); + } + + rv = ApplyPersistentAttributesToElements(id, elements); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } } return NS_OK; } nsresult -XULDocument::ApplyPersistentAttributesToElements(nsIRDFResource* aResource, +XULDocument::ApplyPersistentAttributesToElements(const nsAString &aID, nsCOMArray<nsIContent>& aElements) { - nsresult rv; - - nsCOMPtr<nsISimpleEnumerator> attrs; - rv = mLocalStore->ArcLabelsOut(aResource, getter_AddRefs(attrs)); - if (NS_FAILED(rv)) return rv; + nsAutoCString utf8uri; + nsresult rv = mDocumentURI->GetSpec(utf8uri); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + NS_ConvertUTF8toUTF16 uri(utf8uri); + + // Get a list of attributes for which persisted values are available + nsCOMPtr<nsIStringEnumerator> attrs; + rv = mLocalStore->GetAttributeEnumerator(uri, aID, getter_AddRefs(attrs)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } while (1) { - bool hasmore; - rv = attrs->HasMoreElements(&hasmore); - if (NS_FAILED(rv)) return rv; - - if (! hasmore) + bool hasmore = PR_FALSE; + attrs->HasMore(&hasmore); + if (!hasmore) { break; - - nsCOMPtr<nsISupports> isupports; - rv = attrs->GetNext(getter_AddRefs(isupports)); - if (NS_FAILED(rv)) return rv; - - nsCOMPtr<nsIRDFResource> property = do_QueryInterface(isupports); - if (! property) { - NS_WARNING("expected a resource"); - continue; } - const char* attrname; - rv = property->GetValueConst(&attrname); - if (NS_FAILED(rv)) return rv; - - nsCOMPtr<nsIAtom> attr = do_GetAtom(attrname); - if (! attr) + nsAutoString attrstr; + attrs->GetNext(attrstr); + + nsAutoString value; + rv = mLocalStore->GetValue(uri, aID, attrstr, value); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + + nsCOMPtr<nsIAtom> attr = do_GetAtom(attrstr); + if (NS_WARN_IF(!attr)) { return NS_ERROR_OUT_OF_MEMORY; - - // XXX could hang namespace off here, as well... - - nsCOMPtr<nsIRDFNode> node; - rv = mLocalStore->GetTarget(aResource, property, true, - getter_AddRefs(node)); - if (NS_FAILED(rv)) return rv; - - nsCOMPtr<nsIRDFLiteral> literal = do_QueryInterface(node); - if (! literal) { - NS_WARNING("expected a literal"); - continue; } - const char16_t* value; - rv = literal->GetValueConst(&value); - if (NS_FAILED(rv)) return rv; - - nsDependentString wrapper(value); - uint32_t cnt = aElements.Count(); for (int32_t i = int32_t(cnt) - 1; i >= 0; --i) { nsCOMPtr<nsIContent> element = aElements.SafeObjectAt(i); - if (!element) - continue; - - rv = element->SetAttr(/* XXX */ kNameSpaceID_None, - attr, - wrapper, - true); + if (!element) { + continue; + } + + rv = element->SetAttr(kNameSpaceID_None, attr, value, PR_TRUE); } } return NS_OK; } void XULDocument::TraceProtos(JSTracer* aTrc, uint32_t aGCNumber)
--- a/content/xul/document/src/XULDocument.h +++ b/content/xul/document/src/XULDocument.h @@ -17,16 +17,17 @@ #include "nsIDOMXULCommandDispatcher.h" #include "nsIDOMXULDocument.h" #include "nsCOMArray.h" #include "nsIURI.h" #include "nsIXULDocument.h" #include "nsScriptLoader.h" #include "nsIStreamListener.h" #include "nsICSSLoaderObserver.h" +#include "nsIXULStore.h" #include "mozilla/Attributes.h" #include "js/TracingAPI.h" #include "js/TypeDecls.h" class nsIRDFResource; class nsIRDFService; @@ -254,17 +255,17 @@ protected: nsIParser** aResult); nsresult LoadOverlayInternal(nsIURI* aURI, bool aIsDynamic, bool* aShouldReturn, bool* aFailureFromContent); nsresult ApplyPersistentAttributes(); nsresult ApplyPersistentAttributesInternal(); - nsresult ApplyPersistentAttributesToElements(nsIRDFResource* aResource, + nsresult ApplyPersistentAttributesToElements(const nsAString &aID, nsCOMArray<nsIContent>& aElements); nsresult AddElementToDocumentPre(Element* aElement); nsresult AddElementToDocumentPost(Element* aElement); @@ -309,20 +310,20 @@ protected: // NOTE, THIS IS STILL IN PROGRESS, TALK TO PINK OR SCC BEFORE // CHANGING XULDocument* mNextSrcLoadWaiter; // [OWNER] but not COMPtr // Tracks elements with a 'ref' attribute, or an 'id' attribute where // the element's namespace has no registered ID attribute name. nsTHashtable<nsRefMapEntry> mRefMap; - nsCOMPtr<nsIRDFDataSource> mLocalStore; - bool mApplyingPersistedAttrs; - bool mIsWritingFastLoad; - bool mDocumentLoaded; + nsCOMPtr<nsIXULStore> mLocalStore; + bool mApplyingPersistedAttrs; + bool mIsWritingFastLoad; + bool mDocumentLoaded; /** * Since ResumeWalk is interruptible, it's possible that last * stylesheet finishes loading while the PD walk is still in * progress (waiting for an overlay to finish loading). * mStillWalking prevents DoneLoading (and StartLayout) from being * called in this situation. */ bool mStillWalking;
--- a/content/xul/templates/src/nsXULContentUtils.cpp +++ b/content/xul/templates/src/nsXULContentUtils.cpp @@ -181,46 +181,16 @@ nsXULContentUtils::FindChildByTag(nsICon } } *aResult = nullptr; return NS_RDF_NO_VALUE; // not found } -nsresult -nsXULContentUtils::GetElementResource(nsIContent* aElement, nsIRDFResource** aResult) -{ - // Perform a reverse mapping from an element in the content model - // to an RDF resource. - nsresult rv; - - char16_t buf[128]; - nsFixedString id(buf, ArrayLength(buf), 0); - - // Whoa. Why the "id" attribute? What if it's not even a XUL - // element? This is totally bogus! - aElement->GetAttr(kNameSpaceID_None, nsGkAtoms::id, id); - if (id.IsEmpty()) - return NS_ERROR_FAILURE; - - // Since the element will store its ID attribute as a document-relative value, - // we may need to qualify it first... - nsCOMPtr<nsIDocument> doc = aElement->GetDocument(); - NS_ASSERTION(doc, "element is not in any document"); - if (! doc) - return NS_ERROR_FAILURE; - - rv = nsXULContentUtils::MakeElementResource(doc, id, aResult); - if (NS_FAILED(rv)) return rv; - - return NS_OK; -} - - /* Note: this routine is similar, yet distinctly different from, nsBookmarksService::GetTextForNode */ nsresult nsXULContentUtils::GetTextForNode(nsIRDFNode* aNode, nsAString& aResult) { if (! aNode) { @@ -283,88 +253,16 @@ nsXULContentUtils::GetTextForNode(nsIRDF return NS_OK; } NS_ERROR("not a resource or a literal"); return NS_ERROR_UNEXPECTED; } nsresult -nsXULContentUtils::MakeElementURI(nsIDocument* aDocument, - const nsAString& aElementID, - nsCString& aURI) -{ - // Convert an element's ID to a URI that can be used to refer to - // the element in the XUL graph. - - nsIURI *docURI = aDocument->GetDocumentURI(); - NS_ENSURE_TRUE(docURI, NS_ERROR_UNEXPECTED); - - nsRefPtr<nsIURI> docURIClone; - nsresult rv = docURI->Clone(getter_AddRefs(docURIClone)); - NS_ENSURE_SUCCESS(rv, rv); - - rv = docURIClone->SetRef(NS_ConvertUTF16toUTF8(aElementID)); - if (NS_SUCCEEDED(rv)) { - return docURIClone->GetSpec(aURI); - } - - // docURIClone is apparently immutable. Fine - we can append ref manually. - rv = docURI->GetSpec(aURI); - NS_ENSURE_SUCCESS(rv, rv); - - nsAutoCString ref; - NS_EscapeURL(NS_ConvertUTF16toUTF8(aElementID), esc_FilePath | esc_AlwaysCopy, ref); - - aURI.Append('#'); - aURI.Append(ref); - - return NS_OK; -} - - -nsresult -nsXULContentUtils::MakeElementResource(nsIDocument* aDocument, const nsAString& aID, nsIRDFResource** aResult) -{ - nsresult rv; - - char buf[256]; - nsFixedCString uri(buf, sizeof(buf), 0); - rv = MakeElementURI(aDocument, aID, uri); - if (NS_FAILED(rv)) return rv; - - rv = gRDF->GetResource(uri, aResult); - NS_ASSERTION(NS_SUCCEEDED(rv), "unable to create resource"); - if (NS_FAILED(rv)) return rv; - - return NS_OK; -} - - - -nsresult -nsXULContentUtils::MakeElementID(nsIDocument* aDocument, - const nsACString& aURI, - nsAString& aElementID) -{ - // Convert a URI into an element ID that can be accessed from the - // DOM APIs. - nsCOMPtr<nsIURI> uri; - nsresult rv = NS_NewURI(getter_AddRefs(uri), aURI, - aDocument->GetDocumentCharacterSet().get()); - NS_ENSURE_SUCCESS(rv, rv); - - nsAutoCString ref; - uri->GetRef(ref); - CopyUTF8toUTF16(ref, aElementID); - - return NS_OK; -} - -nsresult nsXULContentUtils::GetResource(int32_t aNameSpaceID, nsIAtom* aAttribute, nsIRDFResource** aResult) { // construct a fully-qualified URI from the namespace/tag pair. NS_PRECONDITION(aAttribute != nullptr, "null ptr"); if (! aAttribute) return NS_ERROR_NULL_POINTER; return GetResource(aNameSpaceID, nsDependentAtomString(aAttribute),
--- a/content/xul/templates/src/nsXULContentUtils.h +++ b/content/xul/templates/src/nsXULContentUtils.h @@ -111,40 +111,18 @@ public: nsIContent **aResult); static nsresult FindChildByResource(nsIContent* aElement, nsIRDFResource* aResource, nsIContent** aResult); static nsresult - GetElementResource(nsIContent* aElement, nsIRDFResource** aResult); - - static nsresult GetTextForNode(nsIRDFNode* aNode, nsAString& aResult); - /** - * Construct a URI from the element ID given. This uses aElement as the - * ref and aDocument's document URI as the base. If aDocument's document - * URI does not support refs, this will throw NS_ERROR_NOT_AVAILABLE. - */ - static nsresult - MakeElementURI(nsIDocument* aDocument, const nsAString& aElementID, nsCString& aURI); - - static nsresult - MakeElementResource(nsIDocument* aDocument, const nsAString& aElementID, nsIRDFResource** aResult); - - /** - * Extract the element ID from aURI. Note that aURI must be an absolute - * URI string in UTF8; the element ID is the ref from the URI. If the - * scheme does not support refs, then the ID will be empty. - */ - static nsresult - MakeElementID(nsIDocument* aDocument, const nsACString& aURI, nsAString& aElementID); - static nsresult GetResource(int32_t aNameSpaceID, nsIAtom* aAttribute, nsIRDFResource** aResult); static nsresult GetResource(int32_t aNameSpaceID, const nsAString& aAttribute, nsIRDFResource** aResult); static nsresult SetCommandUpdater(nsIDocument* aDocument, nsIContent* aElement);
--- a/content/xul/templates/src/nsXULTreeBuilder.cpp +++ b/content/xul/templates/src/nsXULTreeBuilder.cpp @@ -3,17 +3,16 @@ * 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 "nscore.h" #include "nsError.h" #include "nsIContent.h" #include "mozilla/dom/NodeInfo.h" #include "nsIDOMElement.h" -#include "nsILocalStore.h" #include "nsIBoxObject.h" #include "nsITreeBoxObject.h" #include "nsITreeSelection.h" #include "nsITreeColumns.h" #include "nsITreeView.h" #include "nsTreeUtils.h" #include "nsIServiceManager.h" #include "nsReadableUtils.h" @@ -26,16 +25,17 @@ #include "nsXULTemplateBuilder.h" #include "nsIXULSortService.h" #include "nsTArray.h" #include "nsUnicharUtils.h" #include "nsNameSpaceManager.h" #include "nsDOMClassInfoID.h" #include "nsWhitespaceTokenizer.h" #include "nsTreeContentView.h" +#include "nsIXULStore.h" // For security check #include "nsIDocument.h" /** * A XUL template builder that serves as an tree view, allowing * (pretty much) arbitrary RDF to be presented in an tree. */ @@ -134,23 +134,20 @@ protected: /** * Remove the matches for the rows in a subtree */ nsresult RemoveMatchesFor(nsTreeRows::Subtree& subtree); /** - * Helper methods that determine if the specified container is open. + * Helper method that determines if the specified container is open. */ - nsresult - IsContainerOpen(nsIXULTemplateResult *aResult, bool* aOpen); - - nsresult - IsContainerOpen(nsIRDFResource* aResource, bool* aOpen); + bool + IsContainerOpen(nsIXULTemplateResult* aResource); /** * A sorting callback for NS_QuickSort(). */ static int Compare(const void* aLeft, const void* aRight, void* aClosure); /** @@ -237,16 +234,21 @@ protected: * Sort hints (compare case, etc) */ uint32_t mSortHints; /** * The builder observers. */ nsCOMArray<nsIXULTreeBuilderObserver> mObservers; + + /* + * XUL store for holding open container state + */ + nsCOMPtr<nsIXULStore> mLocalStore; }; //---------------------------------------------------------------------- nsresult NS_NewXULTreeBuilder(nsISupports* aOuter, REFNSIID aIID, void** aResult) { *aResult = nullptr; @@ -273,16 +275,17 @@ NS_NewXULTreeBuilder(nsISupports* aOuter NS_IMPL_ADDREF_INHERITED(nsXULTreeBuilder, nsXULTemplateBuilder) NS_IMPL_RELEASE_INHERITED(nsXULTreeBuilder, nsXULTemplateBuilder) NS_IMPL_CYCLE_COLLECTION_INHERITED(nsXULTreeBuilder, nsXULTemplateBuilder, mBoxObject, mSelection, mPersistStateStore, + mLocalStore, mObservers) DOMCI_DATA(XULTreeBuilder, nsXULTreeBuilder) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(nsXULTreeBuilder) NS_INTERFACE_MAP_ENTRY(nsIXULTreeBuilder) NS_INTERFACE_MAP_ENTRY(nsITreeView) NS_DOM_INTERFACE_MAP_ENTRY_CLASSINFO(XULTreeBuilder) @@ -523,18 +526,17 @@ nsXULTreeBuilder::IsContainerOpen(int32_ { NS_PRECONDITION(aIndex >= 0 && aIndex < mRows.Count(), "bad row"); if (aIndex < 0 || aIndex >= mRows.Count()) return NS_ERROR_INVALID_ARG; nsTreeRows::iterator iter = mRows[aIndex]; if (iter->mContainerState == nsTreeRows::eContainerState_Unknown) { - bool isOpen; - IsContainerOpen(iter->mMatch->mResult, &isOpen); + bool isOpen = IsContainerOpen(iter->mMatch->mResult); iter->mContainerState = isOpen ? nsTreeRows::eContainerState_Open : nsTreeRows::eContainerState_Closed; } *aOpen = (iter->mContainerState == nsTreeRows::eContainerState_Open); return NS_OK; @@ -752,52 +754,26 @@ nsXULTreeBuilder::SetTree(nsITreeBoxObje // If this is teardown time, then we're done. if (!mBoxObject) { Uninit(false); return NS_OK; } NS_ENSURE_TRUE(mRoot, NS_ERROR_NOT_INITIALIZED); - // Is our root's principal trusted? + // Only use the XUL store if the root's principal is trusted. bool isTrusted = false; nsresult rv = IsSystemPrincipal(mRoot->NodePrincipal(), &isTrusted); if (NS_SUCCEEDED(rv) && isTrusted) { - // Get the datasource we intend to use to remember open state. - nsAutoString datasourceStr; - mRoot->GetAttr(kNameSpaceID_None, nsGkAtoms::statedatasource, datasourceStr); - - // since we are trusted, use the user specified datasource - // if non specified, use localstore, which gives us - // persistence across sessions - if (! datasourceStr.IsEmpty()) { - gRDFService->GetDataSource(NS_ConvertUTF16toUTF8(datasourceStr).get(), - getter_AddRefs(mPersistStateStore)); - } - else { - gRDFService->GetDataSource("rdf:local-store", - getter_AddRefs(mPersistStateStore)); + mLocalStore = do_GetService("@mozilla.org/xul/xulstore;1"); + if(NS_WARN_IF(!mLocalStore)){ + return NS_ERROR_NOT_INITIALIZED; } } - // Either no specific datasource was specified, or we failed - // to get one because we are not trusted. - // - // XXX if it were possible to ``write an arbitrary datasource - // back'', then we could also allow an untrusted document to - // use a statedatasource from the same codebase. - if (! mPersistStateStore) { - mPersistStateStore = - do_CreateInstance("@mozilla.org/rdf/datasource;1?name=in-memory-datasource"); - } - - NS_ASSERTION(mPersistStateStore, "failed to get a persistent state store"); - if (! mPersistStateStore) - return NS_ERROR_FAILURE; - Rebuild(); EnsureSortVariables(); if (mSortVariable) SortSubtree(mRows.GetRoot()); return NS_OK; } @@ -825,44 +801,46 @@ nsXULTreeBuilder::ToggleOpenState(int32_ uint32_t count = mObservers.Count(); for (uint32_t i = 0; i < count; ++i) { nsCOMPtr<nsIXULTreeBuilderObserver> observer = mObservers.SafeObjectAt(i); if (observer) observer->OnToggleOpenState(aIndex); } - if (mPersistStateStore) { + if (mLocalStore && mRoot) { bool isOpen; IsContainerOpen(aIndex, &isOpen); - nsCOMPtr<nsIRDFResource> container; - GetResourceFor(aIndex, getter_AddRefs(container)); - if (! container) + nsIDocument* doc = mRoot->GetDocument(); + if (!doc) { return NS_ERROR_FAILURE; + } - bool hasProperty; - IsContainerOpen(container, &hasProperty); + nsIURI* docURI = doc->GetDocumentURI(); + nsTreeRows::Row& row = *(mRows[aIndex]); + nsAutoString nodeid; + nsresult rv = row.mMatch->mResult->GetId(nodeid); + if (NS_FAILED(rv)) { + return rv; + } + + nsAutoCString utf8uri; + rv = docURI->GetSpec(utf8uri); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + NS_ConvertUTF8toUTF16 uri(utf8uri); if (isOpen) { - if (hasProperty) { - mPersistStateStore->Unassert(container, - nsXULContentUtils::NC_open, - nsXULContentUtils::true_); - } - + mLocalStore->RemoveValue(uri, nodeid, NS_LITERAL_STRING("open")); CloseContainer(aIndex); - } - else { - if (! hasProperty) { - mPersistStateStore->Assert(container, - nsXULContentUtils::NC_open, - nsXULContentUtils::true_, - true); - } + } else { + mLocalStore->SetValue(uri, nodeid, NS_LITERAL_STRING("open"), + NS_LITERAL_STRING("true")); OpenContainer(aIndex, result); } } return NS_OK; } @@ -1220,20 +1198,19 @@ nsXULTreeBuilder::ReplaceMatch(nsIXULTem if (result != mRootResult) { // don't open containers if child processing isn't allowed bool mayProcessChildren; nsresult rv = result->GetMayProcessChildren(&mayProcessChildren); if (NS_FAILED(rv) || ! mayProcessChildren) return NS_OK; } - bool open; - IsContainerOpen(result, &open); - if (open) + if (IsContainerOpen(result)) { OpenContainer(iter.GetRowIndex(), result); + } } } return NS_OK; } nsresult nsXULTreeBuilder::SynchronizeResult(nsIXULTemplateResult* aResult) @@ -1631,19 +1608,17 @@ nsXULTreeBuilder::OpenSubtreeForQuerySet return rv; } // Remember that this match applied to this row mRows.InsertRowAt(newmatch, aSubtree, count); // If this is open, then remember it so we can recursively add // *its* rows to the tree. - bool isOpen = false; - IsContainerOpen(nextresult, &isOpen); - if (isOpen) { + if (IsContainerOpen(nextresult)) { if (open.AppendElement(count) == nullptr) return NS_ERROR_OUT_OF_MEMORY; } ++count; } if (mFlags & eLoggingEnabled) @@ -1717,46 +1692,52 @@ nsXULTreeBuilder::RemoveMatchesFor(nsTre if ((row.mContainerState == nsTreeRows::eContainerState_Open) && row.mSubtree) RemoveMatchesFor(*(row.mSubtree)); } return NS_OK; } -nsresult -nsXULTreeBuilder::IsContainerOpen(nsIXULTemplateResult *aResult, bool* aOpen) + +bool +nsXULTreeBuilder::IsContainerOpen(nsIXULTemplateResult *aResult) { - // items are never open if recursion is disabled - if ((mFlags & eDontRecurse) && aResult != mRootResult) { - *aOpen = false; - return NS_OK; - } + // items are never open if recursion is disabled + if ((mFlags & eDontRecurse) && aResult != mRootResult) { + return false; + } - nsCOMPtr<nsIRDFResource> id; - nsresult rv = GetResultResource(aResult, getter_AddRefs(id)); - if (NS_FAILED(rv)) - return rv; + if (!mLocalStore) { + return false; + } + + nsIDocument* doc = mRoot->GetDocument(); + if (!doc) { + return false; + } - return IsContainerOpen(id, aOpen); -} + nsIURI* docURI = doc->GetDocumentURI(); + + nsAutoString nodeid; + nsresult rv = aResult->GetId(nodeid); + if (NS_FAILED(rv)) { + return false; + } -nsresult -nsXULTreeBuilder::IsContainerOpen(nsIRDFResource* aResource, bool* aOpen) -{ - if (mPersistStateStore) - mPersistStateStore->HasAssertion(aResource, - nsXULContentUtils::NC_open, - nsXULContentUtils::true_, - true, - aOpen); - else - *aOpen = false; + nsAutoCString utf8uri; + rv = docURI->GetSpec(utf8uri); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + NS_ConvertUTF8toUTF16 uri(utf8uri); - return NS_OK; + nsAutoString val; + mLocalStore->GetValue(uri, nodeid, NS_LITERAL_STRING("open"), val); + return val.EqualsLiteral("true"); } int nsXULTreeBuilder::Compare(const void* aLeft, const void* aRight, void* aClosure) { nsXULTreeBuilder* self = static_cast<nsXULTreeBuilder*>(aClosure); nsTreeRows::Row* left = static_cast<nsTreeRows::Row*>
--- a/dom/bindings/Codegen.py +++ b/dom/bindings/Codegen.py @@ -10747,22 +10747,16 @@ class CGDescriptor(CGThing): if m.isStatic(): assert descriptor.interface.hasInterfaceObject() cgThings.append(CGStaticGetter(descriptor, m)) elif descriptor.interface.hasInterfacePrototypeObject(): cgThings.append(CGSpecializedGetter(descriptor, m)) if props.isCrossOriginGetter: crossOriginGetters.add(m.identifier.name) if not m.readonly: - for extAttr in ["PutForwards", "Replaceable"]: - if m.getExtendedAttribute(extAttr): - raise TypeError("Writable attributes should not " - "have %s specified.\n" - "%s" % - (extAttr, m.location)) if m.isStatic(): assert descriptor.interface.hasInterfaceObject() cgThings.append(CGStaticSetter(descriptor, m)) elif descriptor.interface.hasInterfacePrototypeObject(): cgThings.append(CGSpecializedSetter(descriptor, m)) if props.isCrossOriginSetter: crossOriginSetters.add(m.identifier.name) elif m.getExtendedAttribute("PutForwards"):
--- a/dom/bindings/parser/WebIDL.py +++ b/dom/bindings/parser/WebIDL.py @@ -952,20 +952,25 @@ class IDLInterface(IDLObjectWithScope): # We also don't support inheriting from unforgeable interfaces. if self.getExtendedAttribute("Unforgeable") and self.hasChildInterfaces(): raise WebIDLError("%s is an unforgeable ancestor interface" % self.identifier.name, [self.location] + list(i.location for i in self.interfacesBasedOnSelf if i.parent == self)) - for member in self.members: member.validate() + if self.isCallback() and member.getExtendedAttribute("Replaceable"): + raise WebIDLError("[Replaceable] used on an attribute on " + "interface %s which is a callback interface" % + self.identifier.name, + [self.location, member.location]) + # Check that PutForwards refers to another attribute and that no # cycles exist in forwarded assignments. if member.isAttr(): iface = self attr = member putForwards = attr.getExtendedAttribute("PutForwards") if putForwards and self.isCallback(): raise WebIDLError("[PutForwards] used on an attribute " @@ -3231,16 +3236,25 @@ class IDLAttribute(IDLInterfaceMember): if self.getExtendedAttribute("Replaceable") is not None: raise WebIDLError("[PutForwards] and [Replaceable] can't both " "appear on the same attribute", [attr.location, self.location]) if not attr.hasValue(): raise WebIDLError("[PutForwards] takes an identifier", [attr.location, self.location]) elif identifier == "Replaceable": + if not attr.noArguments(): + raise WebIDLError("[Replaceable] must take no arguments", + [attr.location]) + if not self.readonly: + raise WebIDLError("[Replaceable] is only allowed on readonly " + "attributes", [attr.location, self.location]) + if self.isStatic(): + raise WebIDLError("[Replaceable] is only allowed on non-static " + "attributes", [attr.location, self.location]) if self.getExtendedAttribute("PutForwards") is not None: raise WebIDLError("[PutForwards] and [Replaceable] can't both " "appear on the same attribute", [attr.location, self.location]) elif identifier == "LenientFloat": if self.readonly: raise WebIDLError("[LenientFloat] used on a readonly attribute", [attr.location, self.location])
new file mode 100644 --- /dev/null +++ b/dom/bindings/parser/tests/test_replaceable.py @@ -0,0 +1,58 @@ +# 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/. + +def should_throw(parser, harness, message, code): + parser = parser.reset(); + threw = False + try: + parser.parse(code) + parser.finish() + except: + threw = True + + harness.ok(threw, "Should have thrown: %s" % message) + + +def WebIDLTest(parser, harness): + # The [Replaceable] extended attribute MUST take no arguments. + should_throw(parser, harness, "no arguments", """ + interface I { + [Replaceable=X] readonly attribute long A; + }; + """) + + # An attribute with the [Replaceable] extended attribute MUST NOT also be + # declared with the [PutForwards] extended attribute. + should_throw(parser, harness, "PutForwards", """ + interface I { + [PutForwards=B, Replaceable] readonly attribute J A; + }; + interface J { + attribute long B; + }; + """) + + # The [Replaceable] extended attribute MUST NOT be used on an attribute + # that is not read only. + should_throw(parser, harness, "writable attribute", """ + interface I { + [Replaceable] attribute long A; + }; + """) + + # The [Replaceable] extended attribute MUST NOT be used on a static + # attribute. + should_throw(parser, harness, "static attribute", """ + interface I { + [Replaceable] static readonly attribute long A; + }; + """) + + # The [Replaceable] extended attribute MUST NOT be used on an attribute + # declared on a callback interface. + should_throw(parser, harness, "callback interface", """ + callback interface I { + [Replaceable] readonly attribute long A; + }; + """)
--- a/dom/browser-element/BrowserElementParent.jsm +++ b/dom/browser-element/BrowserElementParent.jsm @@ -63,16 +63,18 @@ this.BrowserElementParentBuilder = { create: function create(frameLoader, hasRemoteFrame, isPendingFrame) { return new BrowserElementParent(frameLoader, hasRemoteFrame); } } function BrowserElementParent(frameLoader, hasRemoteFrame, isPendingFrame) { debug("Creating new BrowserElementParent object for " + frameLoader); this._domRequestCounter = 0; + this._domRequestReady = false; + this._pendingAPICalls = []; this._pendingDOMRequests = {}; this._pendingSetInputMethodActive = []; this._hasRemoteFrame = hasRemoteFrame; this._nextPaintListeners = []; this._frameLoader = frameLoader; this._frameElement = frameLoader.QueryInterface(Ci.nsIFrameLoader).ownerElement; let self = this; @@ -89,17 +91,17 @@ function BrowserElementParent(frameLoade if (self._isAlive()) { return fn.apply(self, arguments); } }; } let defineNoReturnMethod = function(name, fn) { XPCNativeWrapper.unwrap(self._frameElement)[name] = function method() { - if (!self._mm) { + if (!self._domRequestReady) { // Remote browser haven't been created, we just queue the API call. let args = Array.slice(arguments); args.unshift(self); self._pendingAPICalls.push(method.bind.apply(fn, args)); return; } if (self._isAlive()) { fn.apply(self, arguments); @@ -176,17 +178,16 @@ function BrowserElementParent(frameLoade // Insert ourself into the prompt service. BrowserElementPromptService.mapFrameToBrowserElementParent(this._frameElement, this); if (!isPendingFrame) { this._setupMessageListener(); this._registerAppManifest(); } else { // if we are a pending frame, we setup message manager after // observing remote-browser-frame-shown - this._pendingAPICalls = []; Services.obs.addObserver(this, 'remote-browser-frame-shown', /* ownsWeak = */ true); } } BrowserElementParent.prototype = { QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]), @@ -365,16 +366,23 @@ BrowserElementParent.prototype = { // Inform our child if our owner element's document is invisible. Note // that we must do so here, rather than in the BrowserElementParent // constructor, because the BrowserElementChild may not be initialized when // we run our constructor. if (this._window.document.hidden) { this._ownerVisibilityChange(); } + if (!this._domRequestReady) { + // At least, one message listener such as for hello is registered. + // So we can use sendAsyncMessage now. + this._domRequestReady = true; + this._runPendingAPICall(); + } + return { name: this._frameElement.getAttribute('name'), fullscreenAllowed: this._frameElement.hasAttribute('allowfullscreen') || this._frameElement.hasAttribute('mozallowfullscreen') }; }, @@ -526,17 +534,17 @@ BrowserElementParent.prototype = { return; } if (self._sendAsyncMsg(msgName, {id: id, args: args})) { self._pendingDOMRequests[id] = req; } else { Services.DOMRequest.fireErrorAsync(req, "fail"); } }; - if (this._mm) { + if (this._domRequestReady) { send(); } else { // Child haven't been loaded. this._pendingAPICalls.push(send); } return req; }, @@ -792,17 +800,17 @@ BrowserElementParent.prototype = { if (typeof listener != 'function') throw Components.Exception("Invalid argument", Cr.NS_ERROR_INVALID_ARG); let self = this; let run = function() { if (self._nextPaintListeners.push(listener) == 1) self._sendAsyncMsg('activate-next-paint-listener'); }; - if (!this._mm) { + if (!this._domRequestReady) { this._pendingAPICalls.push(run); } else { run(); } }, _removeNextPaintListener: function(listener) { if (typeof listener != 'function') @@ -815,17 +823,17 @@ BrowserElementParent.prototype = { self._nextPaintListeners.splice(i, 1); break; } } if (self._nextPaintListeners.length == 0) self._sendAsyncMsg('deactivate-next-paint-listener'); }; - if (!this._mm) { + if (!this._domRequestReady) { this._pendingAPICalls.push(run); } else { run(); } }, _setInputMethodActive: function(isActive) { if (typeof isActive !== 'boolean') { @@ -903,17 +911,16 @@ BrowserElementParent.prototype = { this._sendAsyncMsg('exit-fullscreen'); } break; case 'remote-browser-frame-shown': if (this._frameLoader == subject) { if (!this._mm) { this._setupMessageListener(); this._registerAppManifest(); - this._runPendingAPICall(); } Services.obs.removeObserver(this, 'remote-browser-frame-shown'); } default: debug('Unknown topic: ' + topic); break; }; },
--- a/dom/browser-element/mochitest/priority/test_BackgroundLRU.html +++ b/dom/browser-element/mochitest/priority/test_BackgroundLRU.html @@ -46,21 +46,26 @@ function runTest() { document.body.appendChild(iframe2); // At this point, we should have iframe1 in background already. // We wait until another one is set to background, too. // Once there are two in background, the first one (LRU order) // should have 'backgroundLRU' equals 1 var p = expectPriorityWithBackgroundLRUSet(childID, '1'); iframe2.setVisible(false); - document.body.removeChild(iframe2); return p; - }).then(SimpleTest.finish); + }).then(function() { + // Don't call removeChild immediately after calling setVisible. + // setVisible on remote browser is async method, so we should wait + // until it sends to the child process. + document.body.removeChild(iframe2); + SimpleTest.finish(); + }); document.body.appendChild(iframe1); } addEventListener('testready', runTest); </script> </body>
--- a/dom/events/EventStateManager.cpp +++ b/dom/events/EventStateManager.cpp @@ -2016,16 +2016,19 @@ EventStateManager::DoScrollZoom(nsIFrame // positive adjustment to decrease zoom, negative to increase int32_t change = (adjustment > 0) ? -1 : 1; if (Preferences::GetBool("browser.zoom.full") || content->OwnerDoc()->IsSyntheticDocument()) { ChangeFullZoom(change); } else { ChangeTextSize(change); } + nsContentUtils::DispatchChromeEvent(mDocument, static_cast<nsIDocument*>(mDocument), + NS_LITERAL_STRING("ZoomChangeUsingMouseWheel"), + true, true); } } static nsIFrame* GetParentFrameToScroll(nsIFrame* aFrame) { if (!aFrame) return nullptr;
--- a/dom/locales/en-US/chrome/security/csp.properties +++ b/dom/locales/en-US/chrome/security/csp.properties @@ -60,16 +60,19 @@ inlineScriptBlocked = An attempt to exec # inline style refers to CSS code that is embedded into the HTML document. inlineStyleBlocked = An attempt to apply inline style sheets has been blocked # LOCALIZATION NOTE (scriptFromStringBlocked): # eval is a name and should not be localized. scriptFromStringBlocked = An attempt to call JavaScript from a string (by calling a function like eval) has been blocked # LOCALIZATION NOTE (hostNameMightBeKeyword): # %1$S is the hostname in question and %2$S is the keyword hostNameMightBeKeyword = Interpreting %1$S as a hostname, not a keyword. If you intended this to be a keyword, use '%2$S' (wrapped in single quotes). +# LOCALIZATION NOTE (notSupportingDirective): +# directive is not supported (e.g. 'reflected-xss') +notSupportingDirective = Not supporting directive '%1$S'. Directive and values will be ignored. # CSP Errors: policyURINotAlone = policy-uri directive can only appear alone noParentRequest = The policy-uri cannot be fetched without a parent request and a CSP. # LOCALIZATION NOTE (policyURIParseError): # %1$S is the URI that could not be parsed policyURIParseError = could not parse URI in policy URI: %1$S # LOCALIZATION NOTE (nonMatchingHost):
--- a/editor/composer/moz.build +++ b/editor/composer/moz.build @@ -54,12 +54,9 @@ RESOURCE_FILES += [ 'res/text_caret_tilt_left.png', 'res/text_caret_tilt_left@1.5x.png', 'res/text_caret_tilt_left@2.25x.png', 'res/text_caret_tilt_left@2x.png', 'res/text_caret_tilt_right.png', 'res/text_caret_tilt_right@1.5x.png', 'res/text_caret_tilt_right@2.25x.png', 'res/text_caret_tilt_right@2x.png', - 'res/text_selection_handle.png', - 'res/text_selection_handle@1.5.png', - 'res/text_selection_handle@2.png', ]
index 167cb6526ce7b354b9a395df1c8b2d9e99918f30..b7159c24b8a6b2b91202f7a3e52aa44d65a14908 GIT binary patch literal 1733 zc%17D@N?(olHy`uVBq!ia0vp^vOp})!3HFyqmQuwDajJoh?3y^w370~qErUQl>DSr z1<%~X^wgl##FWaylc_cg49qH-ArU1JzCKpT`MG+DAT@dwxdjX$U}IlVkeHmETB4AY znx2_wtMq>NekFy>6kDZmQ(pt$0_W6>OpmIf)Zi+=kmRcDWXlvKdpiZ23M-%ixv3?I z3Kh9IdBs*0wn|`gt$=Khu)dN4SV>8?trEmh5xxNm&iO^D3Z{C-y2%EHh6-k8dWI&Z zW@d&u3PuKoM*0RoWTtCqVr6P(Wn``Z1xi5Mic-?7f?V97b^&>|N*N_31y=g{<>lpi z<;HsXMd|v6mX?+vGmMOMfhu&1EAvVcD|GXUm0>2hq!uR^WfqiV=I1GZ_=!pRr6smX zN-!_v7Ql_oD~1LWFu?RH5)1SV^$hfp6#Dw&SDKp(S6y5Zl$wTLb#X{#L8^XGYH@yP zQ8F;%(v(4(3#^=rQWHz^i$e1Ab6}wukda@KU!0L&px_*Arl8@Qn4Fmh63_(e@b$Iw z%quQQ%u7!7bg@+eis)r#rdT<;I6E1dnOnL#x)~W7x*9t=xf)qGo0=G!TRJ;hnwi1$ zy5uL9=BDPA!1Sgd^g80y3rY+S-Kj;HWvMA{Mftf3U{70R;&zKUZnr@6rr>sq1x~&C zK*#8#MKw$an0`P^c)|s8;7LC<518JIfC>AAaYHLGznFWvIEGZ*O8W8tzdbY0WCn@S zY4<J*^Xn!tZCp4b<G>LItAs0&MaQR2Z2h`uLwb?Ug!61i&Mb-WnYNV2=giWCvv217 zbm$WAxan!w9pWu%`aEV!a!T4qw&I8g-l?;%t258EaX90&Cf{>YNb)OAMlrd@nJ3zc zwO)pIRV>Y5nkjQ+miv~aUSC&RM6{@c85kI?Tv#!;b!xoLnu>J|u^V(Y9z4@ACreUN zvhZhsK*qx;H?=hP7`DH^fBrqUd$0H5`T99~*q;1j;+&YRx%k_n*}Hsl=14bA;%ce+ z@c;e%@Au!w`Rt08-XMJP(f=h8r&gH0@8UBSzi+poh2x^&|Nr;<*PHvf-_Pb?jQqnq z`>0RFqX~0oc9*MqiZ#tWb>!gEG#l?5%8s1jj}E13DlhL!w7S6H%T{D$WZb=BgS(Q3 z{%1x*?W!=Kq_D7dv>G#OgRhU?1YajdrYWlzsEXBjU;Xt>o4IzSU*}SX`=2;=tKUe{ zwwO_Hq)Uuhlwo$q7O5*iW=+%n{r^3^UuqhYa;Lb|o)+D|pFe)h&bsmQ_ubR$J+=x6 z$9eCMu`A+_UZy5mv%mf&>lHpVC(f7uPfxv@|G(~c%|s#Y|8;-A{5yQml;6SU3lr1o zr4E0d|DOB#v*yMxpKsn}Z#??QUqQI#zd-WK_rKR(jIP^XT{vmaj+&2eUfy|C>+nom z;a^)E<AW_C9sBKGonP|{7_!Wln!Y}6ci)>?+^G7YTH!nMl}GVMrg$uP@Z;OtYct!+ zUpE)k+wCoGli;r3TT$>oF80Ec{PJ`5Oht~C4|q>}d~vXQ^|Zv6X-W(JXz?A{^hhxE z9n-XZ_i`Q;X&rdP{4inx<Ni4N%1<AEJbSD^t$ZHSH6w-lw(O781AZx+JmHAq;${t7 zadeRbkCyw<h*nkC%931>8I>$M>i5_E`uh9t>G$<}tNutv_13oi5||Pi=cs#R#RbmL zM+V(H8zTfdS{InC$w+Sc^+4R@ox{Ew_9O2nKKdkYc~&$cV6(IC^QNR3OaCuut8ZU+ zWxdDEC-OIQRhER=aEokK63zX;fc1cU#+|=Mulgun4a(t5a5<G^Fl)u4$*nzm{?ETu nG_y02fkC-*ujwj=1_ow^=V}H!N?SEbK^2RqtDnm{r-UW|2LrE=
index 8b2ec905aae4474fb2507831b0774ba1d6900bca..f350cd393d94a043c5756c40e4b73b93d8ca92fd GIT binary patch literal 2284 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
index dae08f27565737d34420b9962cc1031e1e5e6936..a3b4725cea1ec4e3a891ded2a5d7bd70d70b7f50 GIT binary patch literal 2947 zc$|G!2~?BE77mdmP=aBzEP@e17Rd&Iutl;F5J-R!H?T-ZejtV<kOadbhD}x%z!Yf& z1wm96OHr!@6vPEZjL3Uv0ReFX0Rd4;RXo06tM8q6j-4~}&)j>y@6Me&-<k902h)9_ zI%YZ`5D2=-kHS#RMAe`|G*#cj^Y1>YCR2&eHc6;3S|VkM01%NQi~``B_^cR!0kAm9 z3AX_+5NL%pm$^-{jTY#^7V?p-WehTj9}ik?UfxOZEH)32z@vZ|t^jW}+uCjg=W_5? zTU}^qT09wu<@%+FfY1~=lbyn2yK}6(H^RM=JX8VrfP@84;>QWZ9!Yqs&v89e>t#2} z3jP@);o+_RN@^P|7)}<70Jsa%8No(lFmP8_BnIo^>WXoMJE1X7D731(A~0AF7grA_ z9Q^NQrP@uziS}SnsDJOJvhY^15=p!V3MG|Fky2-*P!xm0xVyVA$8d5&s1OKovOvO0 zLI}j`zC@q^Vz!7IFX0LW@MW77B}|myt*pKrfgk@htU&yCnN$TsC9&dB7$h3?KR_Do zpP_vISF~8d0HXgHEoLUi11JU{7AA_=s*8(Ww@i-rAd3K&L?~hkg>hf17#u5<2*t6& zcsM!K6>djkvAKd}hy7;;jpngQAeOKMY+w@wZ>91exm=D1g-Sw`aYQVJ>aME050;2? z!%=ZKH&-&%%@ytD_JvCkvJ?4$K=Osl`7hVyo7`nF{KW=D+}!|&DiZSHpAYT9{dO)+ z-_GSPF6Z01IDeCiQYC}>zpMV@Qt4-T`Bk_o<E!)mflBWpm9Tg20rNp1t%Xe#A~R`l zTA>|hwArL9W~66iczE~4-l!_0up$i|yCeBlxeR@6og6YL*np5>cZ!&K)5OMvYz!gt zt{U7>k2NCSHwtlo-ZU2XtcVf!9X>r`h%aq@_~*!cZ5xt2Fi=rZ5xH~Tsnz*a&y~@o z_Sv-2Ma#PhtNTN$xh>bGerk0%9UuOBzvcOkk8Z__u!X>?*Ud~_GfOZ2hspE%@(b6z zJZtay`d&f8feG9bUimhYVk~R}XgIq4XZF+9^2*JVX$txN(0s#j)3IdZY26lNO^GiU zyb=V(vL5x^?tS3YbQiFuhs`-G)m)vF8>{DKxKze86L@7o$Fid_-&?*_hs>Q;kO>-T z8;88*(Z6F}?aiYuN*4DGtQ+&S;I2Akr0&Rn>qV`2#JkW7IzT?YQ*WhagQmLLEtUa6 zu(MZsZ45Kl7-aO`^FeI#uU(!;i%I93>YvX;MT!+@sEl}OM$vMm+S>_g-ma-!_1YxU z@rv3luIBLfX}@T`2Z!f+`&9=8gkMOr`2)Jky$5`Yqggpx8fs{AIbplqC#`-bPyXhS zq)tZy_AdCA8k{|h`p7JOCY%e0vh=Emj~-(N(3Q<B+cO30Auu0M#)ng~Hd~*EypA9v zsN;zjj2xHkwmrke*fbyo2JznogBO6x-d@naOJ0)o2%yW?bR3wIf*;nmUodnyAq>Xa z+e5;-9ZRqoT{ugN8oK{Seei_o%R|rFj`^pCj7`r&3zZ!|H#ViR7OMj;`O1C#H8a$E zW#O-xHzSq%KE;&;MwPb@Y)$}f_e;<1?SOlYrNhinMSa@qY2jxrE<GJwU#2xJHzGXd zgR)%vONV4r`|d5I<(l4(bm*J&h_aazdh-`9mR{-!X{{uio4@+~+UTU$mcz&K<8-1D z(EwRfr897MEvRz5U`IVS>49*L4|E&v6<IFcsIxd8coxx9e-++Vm0n~<0ukmL`V;aG zuQDQN>@V@OU2IhZl>(@;nM?6CYI_rN!6bi3n?>^qsXVTBv*I0dzvAL+<>HZ5UK!2z zR)Mdts5r@l4LXs+l|R|X?K$!TbAR}o=O=Ih-sLU1ps`dK#6Cu=Xlc;FkoRkJs7~zr zw^!Gg#;r5F*65$VueVe`tT|$*rUgrCLr5<MMBPh4-#?izpE%>Z+6H8@vdpxCg*jPS zOQ#=YPv9OEI0v~t<It?LU5%~1a6Attv8@E&F>t#aEAE$9))_&3pkrk#?o7~7p*nGj zuD)}5QM%?(F{B6bKwDUr9_Hzv4>MtR2J;6&yx^Og0t9;6)u*7jZ)&Oeu-J~%2zHFF z4)VI6Mis&}@<3^ZJoD&!o2&-vK5Rf<2r2S;-h+w0A9XWaY-<fRW@rQZqkaU%nw6<M z(afbzq&dO{Z4?<;kW<-rU4vK@p<O;Y#c)zrjxXqEX)!jejdxZ~akpUAFP`(!O>gRq zmIZ{KELodm&g0n^+u-iQyb?E`^v~%H+unJk{kK>G<k|RwY}$*Q?<l^*g)Zd2L504m z?=5G-{yh55{NQZkf~UnpBd*RLgGZHpPc$bZ#$0Mj+P2wL9!&|aKl`bU0#WkDgF>Zu zcoF^-4YNG;bis7}@bN|G8wnABY38W36+RA0yf{Ctzk<|Y&`ZNF<jxB(@w)eJmFxHy z7Aih%jGzrSVp`6mPU)A}XIRP81tTe`qYsDH?m0vPTMO^bOoJz9=F1CPE(UgOATqjs z*Q}c9klH9q#P9smAG^sW2><#m3s(e3XGhxj&6;4iF$3?DlY7^7Nowc*y?Lu#n3B3i zvo|w!uKPmGRfCmk#k*6?XR%veq&1bkIq!M{oZj9)u;Z8H+gsd#>T%rD8Qn~HS`&pt zSjqQ_2nN4OK=jy`H@{aqG^DYUpeM<>rMtQbrw_AiG3ZCy8~g^ov9&y;u0Tg&*GNwv zR=6dM<UQLy+n95vAw*AuDF{FOUG=rXxebFBI}20s)4O#$*Kzb1h-+%)btfhdSY{DE z6g$@HqXro-ehybE8t^AHI>Kf3by0Wvyt?*PKQ{rHC0WeuuKUP%l=cF?5N7QVRvC6I zBbWl_(WgsV-rjN79Lh^Dg4PTf-gx)%;1jm2RJfz<;dpG*Q#D*9qz$almci!3IM(%j zpsZd4c<@UoqPy(r@#Gs15+_T}s2=G+hvo|F^7<#;Cl$j}-@TF<M|3fSoer@bzLx1z ztaBvnAGT;>0onlFi%~B9JK%ETb}@CTPs>~Td`?k-_}0^lJMgJjenrpLe0-asvp4J} zy_c?g#okZNf|`Fno!~&qssL8!T$Pd^dqE{Izq*85Wg91Jjty51Y|EgY@-5V`n^|lP zdn`|FJ$vE(=w$6Vt$<fPAB3H6_E<iC60^=L_KK%^Erm{pkl=rA&&p+W_~JL7(znaZ ztaQs>dGGQbiN88YPd9T-+u;^j{lSKzL_gTGhN{l2{wEnz+UTVy`}W9g2w`qaX-h&z zbs#9Qj`B6vIZsQA985^YyL7XsckQ9|tErFZK1yohNVQIO>(8>~24L;X^&?o6%;oh@ zvn!pnQ1|f^^@+9e&Jh2B`(|KW0*Ix)q=qP}Dc*b4S0cNVbx0#SQbsgfp-DXcZfbRv zCh|1ZPx(RLIGL?BMkHh(ly?wpOfQwd?W&CiwAThgKIJ~pt}-OqX44@RFpsqo5-jeM aHX1ZZ&SoR!_bAK1#hZNSl(VGB%>Mw1;m)=I
index f3151a37a7910a5a2c630ac447c1ce7df03f5d50..6599b66391d1d381a6d724d7ce846b15e1427cb2 GIT binary patch literal 2692 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
index 9250b816288d13725a21da76d8c99f72d6f0c47a..1259d00f770c74afdef2f6d4b498d71ea77b33fb GIT binary patch literal 1655 zc%17D@N?(olHy`uVBq!ia0vp^vOp})!3HFyqmQuwDajJoh?3y^w370~qErUQl>DSr z1<%~X^wgl##FWaylc_cg49qH-ArU1JzCKpT`MG+DAT@dwxdjX$U}IlVkeHmETB4AY znx2_wtMq>NekFy>6kDZmQ(pt$0_W6>OpmIf)Zi+=kmRcDWXlvKdpiZ23M-%ixv3?I z3Kh9IdBs*0wn|`gt$=Khu)dN4SV>8?trEmh5xxNm&iO^D3Z{C-y2%EHh6-k8dWI&Z zW@d&u3PuKoM*0RoWTtCqVr6P(Wn``Z1xi5Mic-?7f?V97b^&>|N*N_31y=g{<>lpi z<;HsXMd|v6mX?+vGmMOMfhu&1EAvVcD|GXUm0>2hq!uR^WfqiV=I1GZ_=!pRr6smX zN-!_v7Ql_oD~1LWFu?RH5)1SV^$hfp6#Dw&SDKp(S6y5Zl$wTLb#X{#L8^XGYH@yP zQ8F;%(v(4(3#^=rQWHz^i$e1Ab6}wukda@KU!0L&px_*Arl8@Qn4Fmh63_(e@b$Iw z%quQQ%u7!7bg@+eis)r#rdT-{89EyqyO_9{y15w|x*9vW8d+MJyEqvdJ6jr<I6J}g zy5uL9=BDPA!1Sgd^qS(-3rY+S-Kj;HWvMA{Mftf3U{70R;&zJ>Znr@6rr>sqF;2bu zK*#8#MKw$an0`P^c)|s8;7LC<518JIfC*cKLA{58f$62Ei(^Q|tvAzsGlU&Qj+d8B zp7Ct8Tkd2wLDsEe_5wCsayAtgoC7~7R0V$s5RF=)FmKP1Xh&~W|E3PHDM1<@{8cNZ zw%B$mY!s5-eCyd^6W?1ZZ`15+l=BOEJvZOn`sC4p)93td{XF}B?@V+3Im<F-Huq-y zmapS_x7++KTkN4@InUH*zio1#5Il9$6v3nCo7KHaYzoR6O1^x1`=sKZhWiQ0L)^<P z4)q@hH$RYKaP*prhD**j#~CZc)$SZUHrKjpNAqG%uBi!gR^3|ie}B=NzdSa35?nW! zE)FPq@bQkR(v3Xl{cFF)3y9b3TGej4b5d`+zRH5v?n@_~75L!!M{mXMBsHEOBfHgJ ztKVKYn;c`hPw_+Gn)^Bvwr%|xA@#k_>+wy2TedPUTO{sn+Eo42X`92%gw-$Zl`cH; z?)@IFjh`ZHOhsF~+@^ez(Vw%e%8&K@GqaEtiwa8@F6>T}e{)T4b-?dw3GBB|?7w=Y zaBcDLcMW#kGjjJ_e(TD2{@Lo)TYh}ZeHg#Q(RXU546E~nE0-5eTBhY4ws7i}9SsJT zPVC(;wo>Q$vNcYh-_NT*P?6=fz{}9eHp??r?`!SLuRp?VjEWZD^$uUE(x&IG)qT=D z?eI)VdCB&4&fK*RCmx(S^@w$?`PENems+CZ)70eyu3zHanIqy=9xEB6Q@6Xmmf?}M z)aB)wKTjOt{&0z9k8aanhZRR2d3_OOR;-utZtA?{uAjZzeuH|6X3f3g7dJfbJuJEE z^Nu_InNjlL$vao32UeVI=*$p_N%OkiY~*htA+g50;J4Gi$Itn8f0uhHX7jJicbbUi z+!H!`rMkry8*BE+hk7_0@lQK?I@)m27AYU&(_tHOD-^3YIz1@u+}ra#P3-E^0-tO0 zlF9*W+b*ACIn(UGzwdT|6KCk9A8R*Ua#_D2Qls0j**ajUZ19|8UV<#4C9nQz8P9Hf z^>B$*=j(NPTg5!?pP8~C=j*DMv)i*BvpJ{aTshzQ|6Q)+m*ou13{3A;wzMi=lLS=- Mp00i_>zopr0Lo{GZ2$lO
index 9837cbd0feb5350f07f24bc902ef24c879058555..4e529badb4f47efc85bdc77ea04262a82a30ddba GIT binary patch literal 2146 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 I9;}Ri13>I^`2YX_
index a76c610008827d78cd01f125f2e8438309f366d7..43244bfc79cce347e93feb903c18f7993eb2004b GIT binary patch literal 2676 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 CL|@hb
index 5ca79fc8738617127e44d306b42dfb692d19a8b2..f0a0a5dc8e4edd3d44bc8763deee0cb9cb015301 GIT binary patch literal 2588 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 bW6Go)7*pafb{xg%%>Nq8aT7D&!9VUlV+B9)
index 965c749d2c36377e448a7729862ea1037228038c..076276acf6f83f35f2b8e2fe4e38db6cf157fda7 GIT binary patch literal 1688 zc%17D@N?(olHy`uVBq!ia0vp^vOp})!3HFyqmQuwDajJoh?3y^w370~qErUQl>DSr z1<%~X^wgl##FWaylc_cg49qH-ArU1JzCKpT`MG+DAT@dwxdjX$U}IlVkeHmETB4AY znx2_wtMq>NekFy>6kDZmQ(pt$0_W6>OpmIf)Zi+=kmRcDWXlvKdpiZ23M-%ixv3?I z3Kh9IdBs*0wn|`gt$=Khu)dN4SV>8?trEmh5xxNm&iO^D3Z{C-y2%EHh6-k8dWI&Z zW@d&u3PuKoM*0RoWTtCqVr6P(Wn``Z1xi5Mic-?7f?V97b^&>|N*N_31y=g{<>lpi z<;HsXMd|v6mX?+vGmMOMfhu&1EAvVcD|GXUm0>2hq!uR^WfqiV=I1GZ_=!pRr6smX zN-!_v7Ql_oD~1LWFu?RH5)1SV^$hfp6#Dw&SDKp(S6y5Zl$wTLb#X{#L8^XGYH@yP zQ8F;%(v(4(3#^=rQWHz^i$e1Ab6}wukda@KU!0L&px_*Arl8@Qn4Fmh63_(e@b$Iw z%quQQ%u7!7bg@+eis)r#rdT-}7@1hQn3}jcx)~W7x*9t=xf)qGo0=G!TRJ;hnwi1$ zy5uL9=BDPA!1Sgd^qS(-3rY+S-Kj;HWvMA{Mftf3U{70R;&zJ>Znr@6rr>sqF;2bu zK*#8#MKw$an0`P^c)|s8;7LC<518JIfC>97>kDsSe&O<TaSW-r^<<iFhOnbZLwcEc z?xiJ}Ia#ZxU1_f05+Tyxq)@f7sY-<9Xpn7%rpT8i9pa8}9XE2giUclEn71V0%OX*& zMWNkS%tE8H>o5FUel~vc*>`>UUv%@FH_a~TZnCR5c_z*Hes%t{z2;lK9h6`!%sRPa z{rel!Jnb6Od!A_QZD-!O&r9O>R-LE2w&w1b*%hQ5Je5yZuzrc#^Z1_a^Zp6Fu3!0S z`l{&ryIlJ9513qCd$L>jpzv+AA5wRAJaszyYPM}@kY{A|Eoq~gV~(;dLEkUk-?Bc+ zE6=}&FGkj?x}m*G`uL|tucp(F*Y3P4&60oG)9z4&Q|Fmjj(K<LWYg|=oaI^P+&aHZ z-;J~73-8HrjZ&|;|4Uy*zmIk)?`*ej=$p~iFl{A+tioxLl{>a19zOcb*r4P&)3t?M z!A~@koVm1C&GPy#caz0geBEr>u9pjgRw`t&d$Ddjv48zP5uUww*o^Z_yG<5ec&ci4 z^kMvV_I+U=_dJhGWL0gt9GfbxwQFyvz%wohv8PN+T?GCf*%$Eq*DS9)*VnMWGM}>X z3YUXt(h~dD>MYqfQ#rXU+$k$leCEl77Q`MDlKOw+ZszUN7aJEW)G|DM+4|b0AB<X; z)vYgF_}m^F!519Se|F)5ldbQLpWP|)PABf$dApsr*$+#^pI*bM)7W=E<5f?6-Suhu z0lh8H+}2&W-=4@)BDZ^T(o@aD8(iAX$?g(+Sy{#Jw|N?O0H?2S-QJo#q4VUP+m^Mt zOO{;J*&~wA_lHd(p>Ap;+o^BnZ@yhOmC@DU&pG$S-PiBP;q_L#_T9Do@6)U6b9nLl z1<DI#<lOutkJV*01f9{nr&+Nu;`G(v-Pw!tpC6R%yE-*{#ft)shch#DepE^DomG(8 zBf+#xU={P%dtN>Z**3Ju{l1XBPW8u`L>uNE?TS3Fb<P<xZFahBqjy9qswXPtg`7<Z z=LL<XMHc?4%**1#joxjoo9g(UOK`>YU{OX-=eVq}mualN<_;d(xjc%~uBPw2AS(R$ r=$ro2@r_=)PXFJt{6GJfb_QmK6$T-uC3{UYKy`(utDnm{r-UW|V?m1_
index 9018291b5c543ac9adc1655227d0e5ee7d4e6f67..6da5782e86ed44591466a07e745ea2d7c9d24e4c GIT binary patch literal 2191 zc$|G!dpMM7AAaSSvh7AF%3Q;$26Hkq7&9bBrW#piu^2NiCT8B*nO97$Qz<E(sIAy) zrE&=46e@?IWWyAaoK=c)Ds76|>U*cs_g&w$?eiX<=lNateLwf}dwzdBDQ>PCv@};} z0sx@Jba8S=dUNE%X{aIpli#nABHePB;R|~}p>Q-?1Og6RC<H_?`Rp*z9b|JQzuW?8 z0HAEb^Yn#%S(~XGh>v9}FxVKr5KuIleT<OJi2z|#2pGl_(9zGY)uB;5E*<SdX5m>v zM=+e{vO@%V>~Qtu?1<plaMAX5C|V2^5#WO`8x_Nk6o{!Ybo3`(Dl%4tacI;h2pmC2 ze>UaIazi;nA`nH!T3T@M1Okde!4gPh3WZ>fBH{@|93FWo76cNNOra93P+uAvSxv+Z zrMf#ge_0Ee(b3^BETrOa(b3V^XiF?43d0d>Y-|)7M4|<Pun<cGFgwOVAU2*?a00~~ z5l;y7AOT7-XNN#hFddDa-vVFwRaPMWk|rc!xEQt&N5JB7{{v*P{vFEae?^O7cQEwd z(PB@D5X8BIVkk<)K@Kj|SV0z29Yr7;hD4qa6gi(ow{QrC#Nm(-<>)~{tzoe_Jb@yx z_7j7}qA~?ym@VLdOeZ=TiC}p=E|N?J1y8{f7!J;s1Omg-VLgLr?MNmOtZW<|Y$%j@ zt`o$G;)4Qsp3D6&m-tPtq8L82K@o2|$aNM$eAK5+Q+fa3#Su>=**K9Yh#v8uy?o|! zzwHJ8O)oga4DSDqdOn2Ik7D?>aFNN^(gy`dy^D~-zMwN(3jnH0OeY7=nBIw#{74;! zepA@U;0}6Gp^wYabNlwJN!*M2VJ>tZ+0@xSW$BHl>SRLP_r^w~+F+fwj0dZ=mvVHp zTl4fEnH}+TTd#pyOSs&jr%dk+EZXyW$K=4|K<z+L*^Hm1)zsi*;BzeDS^DPa(W{y7 z<?&}EBNDaj_{G^FmT#$V42I5D-j+7CuP=yi;{W(jI^KA8CZpKkT!|iGnwGoP{ngF( z-Ay_1fBP!A)Rw%c>QsLg410pt+gwI{II$YTGx!1~OLu2^z{oLuqp5?F@uFiN@=G3= z1zC3&bBEur>>h>cHFlDJI?4R_A;Nt6+4!!3b%kmVyxH2_We*Qrg4jX9Qn^3+z1ZV^ zInC6>C5V@`JEAnE^bfsGd-B?NqJ2lSNd+_-HEg9nQKuYQZ>D6rH==)f?4j4gombVg zW-;<J`A^C=c!V9UNuQmNFYNWPo|QD3slIOSTIV&Dg@N)e-g&l_?QFQRJ9g%B@|L!u z*2MAc2YS9cS^Q#~X;jhJfutC8{k_v?rSD(75|<kglavxQhSLrmEY9{!ONsL|_={97 zjjiJhRc<lY$-uKWwginEtiMc5WeVjNrz)D>z+2a4?@!oMzv*u7Ugv>q!ppY1&pZ5$ z8%P}^$u$YqUOjNi+;Q9J=^RpiiRAiGi^)6DP<G{}@I||tS6Rs}^6C#ARJm9GbZ^=2 zz=qW3RXMe-SCa;-XO0T#39*Hhf*G9u6`cfw{@hkfe)3drzxR^hv6fnw`yQE9jiW2{ z=YFe@Iz*Mq_S@RZl9dhd+ci^5?$;Fr%FYw|HOES2H;y+H17jg1^LGz-+G+5M0z%?2 zu-C1wMOCQQCqQRQ?s)T$M>3c<$5hRSQga@>yCK*8S%oIXyj5o%)|>%cgQik?j)h?p z;_cGf&-Y*0dOcr!SGz&hXj0bl8_Uwd>bvY)R?dg-t+4KkFVYh|+LhTqYj2P|{c?5# zE;h&PCATn`SGZ=g^N{6B!Ll@o>Xe@_>`Kyfnzh~U+I!>P{oyUk3#z5%nXE2#i**r_ z6Qe4z?CSGvn7Jz2vX5E4#gAspe3mA^Iiv2jwfh{czYI*bJ&|yt@o1${pY*otjJHK9 ztFb9WeLF*`b6@>}0EXL@2R9B&g0(JgIQOJuB1js!)6dX=S`rL4B_~U3rk}3fMm?qU zr+K+yj?nLfOgBu&u6y{P%C$WeU479;y{iWMXBID1$qevn%}3uk^YH`xciS1u@?UQ& zKQ8#eA!~W&1~pIbLfz9|r|!I=11pnE`Vwjzhbp$s?GIZLxnv*~(pyNPoJdu1w_Ck9 zpo+G9($4IV)@|U(!v#FRwVtdTXWqAVLZv(DwQ_kDT-H;JYtxYgkG^fY`mU=ThHa+R zc}t75S*<C}aL7jeYJ3LuFS&+RQ0Sj&0OmT7lXcc?wC+iQUpOus*65`8@4E%gZEgvu z#>gHknO{%P{Jped`u^Cfr9Fn1bZ7OojPO2^BYC>jtqVfW9@h^}Oj(?!?c0Lx64VI$ zF5FC_EwpPk9ojA%0eVcY_4I6Hq@*>K;3or3pN*tLy1tx>z*9CJ9#_KMbh}cP^l9yv z|6J1w(E1%TY>ic&<>%5j<urRQXth7EeBGtT{-XgKhc6BuUu5kE=OzUi3os>eSybkD z!<IuB`s+f93u9T+$9mnwh`6`6qz#NcdWnkXzyzeE255%-q*O3<x?gccU@}~tiq;43 F`VWAnqjUfO
index 83f4b931d7ecf1e8a7b8456f1b1031531b668354..f8f61d22116c7a5362cf460d5eaa230fd187182f GIT binary patch literal 2735 zc$|G!eK^x=AD?*}!idf))l3dY?agenu^97KjbTKmwAqFo?WOI<ycP4(@op+bsHMDx zB&92K5WPrt(os<;9i{Tr;V4p1dZu&gd9LTWI`{Sa{qFnwy*{7s{kgyQ=Z{|&!`I7D zf0aH61TysTrUj^Hs`~YW=%~L3|6KT}o>l@LAwZxg0+6vKJP?&D3g^Ln1nfv&0FTX$ z-_y%;27xr!^O+$)2z@J=BNCw4OBhs~Knz+M&Mt9cHYb_~z{7cwd?5w#M_UI1&gW7P z+wgP@UF^<_;(I4Zc!3GNOin^Hhr~s=Y=S$-k<|qRJb(?46YLgB$#E3K=elI|dMS-Y zz&}HPXbR#lQ6Y2&++8H$!SN^@l7qow;RFH->wqT^up8m_7_2=SqkafTtOFTOAlnn+ ze-DIuHwia_96<B@doOiFK|}$7n2bitWHOWthZ0F5(O43Rv{b|19;rqkrSU?59fuT3 zt-e&C@uVCHUkva?Like54j07$6a?bS5d`9|Wrfne+oWz7I*u(yV^J9N{{ZRqe})PK zU(r$^fEV%4Xel#Z%tHt8q@oxJM}2V-R!d|t*<He810o4iB-;I@i;O4{Ad*Ij#BldO z0^F9)=J16}i61^Q=ybA=Pztbx9G(x2f>38rd_I?q$Jo<wI4TkANy1{W9u8EZBhiyc zbR@WYIubCBj$gPm5hq5#69Qkj-2ZYNzsX%P!(VKkgdfY}dP+nB_~%2D`QO%p|F)LD zxZH1RA$*gIR*OOZ-&KD}sr|FG{Ayfv@YVV}q1x{fwXt)GZ=gURT}vMtl^OToU!~Ez z&4SFXPd2rUwme*MqVlYn7Hva8MPbi7jjJK}eXcIhbXp$GP#^1`TL{}4=e^8ZnT0lG z3|3WzZ$W4kLgUS$8>_AbEUT<UHn!{*v<c=(PENS7(AV{mjkwW_vDf)zBB^AUFN;-) zK245D*Y#nOi7Q~$oasM49j&t69MEh6f?XMZcX`@hXR;}HZWM2Q(3gNLcYcyM1hE#& z1S3vi`pX`mL}B;5QWScD<=dv+VRD!+UVqOm*zI&^@rAnDGFjemj_TDpC>i=o>`Yz$ z{E@+v(&z2zNFy-pg>#*zCb<zdY7+$SJ*2!z34;6tnQpGzeo<^RS*l0O%D!*?sS|r* zDxkbN;^B+VjD&Tq7TT@{sJ9J}5LX=r&vdgr?7ZdB7MI0KBmT}$e`de7?X9@|y+K#7 zR`r%FNSMdrBs;sqPtEQf!|%V~lx=du^xm`LYvPd*fO4E2ddz>q(t6b^|BAxc9~^;A zNf#C-?q=U#HQ^o71Fh&W0b`ju0*KKjn_o$@Inv0`F@hDcWVJjuf$|~2c<;{Pwq!4x z*7e|=)Y~|wwjsnK$x?f7?ojecvOuplLiR9cbFgpHzW%H`e!uP(_m}L-MNK05sLz-k zu(EazWrf5s;{5S6tL67xcfMMDQyr-qx3!Cpe|hwDcVMs}8N9Ps6Mk-x5hOi4*M6QV z=AQd=Fc{cSar;2wne2<^=FaD*XWK4XY|jXL#tGss&sG|xTD8`P*k>EPEmwuSO|!3^ zRfRSl*#}58^^br-h#W2K_RX2rh=D2oE#T5rG3M>?%2MNB(%h}SOZRlkSC%h-vN!fr z+sWZ&F&$FL3xmK%4LkH*6$|p4A^U%;9XmCawyWjwY%cNsUW2h!^MhNOsf`*`#x^$# zT1^8sH(RNJb7hgnNc#p>*HH@!=|38lsg%#E5bH0PI(Rjko2ihYp)-z!>uYPN#$c6U zHmfRL-mbeg(a2SNhpAbOYi)~HNeDRV*{20<)z3@JrE*i#(>_NxgObO0M3h>)WiUyM zR2}&`b4VHPhzGteq?sz-T1+q=G%zYxD0SISa-v6TB+Vs;;Nzu#s4mZU&A_n_qTDn6 z(z$-<4ZN)ncJPe-bnW2X^2!tGL-coL;ESP_N=X7DcI|%fTQO^<^Xj3zUwiv>^&fj# z+4Q{s(cx4-ZiChv?ctcRNd5*n)w-D#43W?jkn6R<xib?LnQk4SmfmuW<3^yLV%(A> z*5pCi-Fj&HR0HLfOZ9^JcIcD-<ku4(2kjaEp7e{czm~J<jc4JpqYZ04a(57R+P-T) z#z;I!?)kZnr8l+Q!K?6q8O&KofQ(1?Cu6b<93~|2(G$SgN_OKgJZ@^PtP8|kzOuZ? z?x!sO%L_$<zSUgIA?^xL_%7Mc^Ktam<UCed9n)&FV*j{uA1J$HO_hIF)2MFK?{*-0 z3WiLgepJl8%`x_-K58t{fnM5kX*lr50cDl;ZZJbL1~Mb=JY@CJC8)vOnrqA1TVa+5 z1fBV`!s!ecGR~pdSk$;F@DSo@B3>6oNGkpT^3m&bbW+C}|D20(g6Wl-H%5AvC-$*E zw0g8<sJ0Zn(0LqP?=*5@>hkYH3!oPG4LvI(?e1)o^z=*@%wX&L-Uc_>^<#hrWJgV7 z+qw8qWb8-*GNlM)$!LV-4MGDd!j-(cR;%3wc}{^37Q_#IT>W*Xl1B;-{7EFrx-dr< zn)I)b?O+45XS;XYk3Bmy4IC!-)u~)3FYQ`jb*(8th4zukm^PfJie%;F2fx#cBNso) zlpym?D9EI>C`s`$cFp&CkhN(H8{*xPuo#V=BHZtbdas|BhQ;`nTv9xZyADYN4Lkx~ zMop^j>N5NtGCTITq$o2|?@_LVQGF2gCRA2&&uBwzJ;a=ruhWf>Ts-xpQiS%z+$2Q` z$B~AaslP0;W`}n<UwD{bai#t{Ku%SrgCLq3wyweRn1P|@LpRU2wvdsg-S<O~^8GC) z+~ZB{ucoe4Z@U#!)PH=^w#=bimbXV5N_r`5`PWc)W}(7t&e+@iBc-OAKVR;&rz`PP ztKn5jJ-A!BVHtR`7ghK&o0MqqsatNL5p<%Y#s1J$SffGbtFD>U+!QF-OAfQ?(TH*y zzEi2C?F7fYFU5E~PSNS?f}!&jlmhP*Xy;Vs*t?Xuxkt=xC1wa`vx@e67J8gX%an0= z=lsFLcQ#!EGSUVTS$8JnzfD7u?OdO>&ocwGMPScL_9<=0>$OZc>OF_I-JJMWCWL zcM<j8#INGcGTn&cn+LRAZ>-CIPS2@6-$z-a`YlCCw7R+~oqlc}`y#fCpmmbb`y3q} zm5cbatv{7T?If`ss&zJ6hd?(%R%jWdYImuVXW?JN@s9h8rYw-&huWMb7|3<$7t+VW Kmv+XDmHHoQUTN|G
index ecb4c54e9c69d1abadbfddeb2195cf9837ef803b..1d7553d52a084553aae96bc20d1b265cd55ce344 GIT binary patch literal 2596 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
deleted file mode 100644 index 656854decc593d3ccd8767654f1743fb2523b64e..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 GIT binary patch literal 0 Hc$@<O00001
deleted file mode 100644 index d99b3c93b6c9471deb388c0693250dcbc77fc0dc..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 GIT binary patch literal 0 Hc$@<O00001
deleted file mode 100644 index a7fc3901ae69ac20299ea421aab904807801831e..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 GIT binary patch literal 0 Hc$@<O00001
--- a/editor/libeditor/CreateElementTxn.cpp +++ b/editor/libeditor/CreateElementTxn.cpp @@ -1,158 +1,141 @@ /* -*- 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 <algorithm> #include <stdio.h> +#include "mozilla/dom/Element.h" +#include "mozilla/dom/Selection.h" + +#include "mozilla/Casting.h" + #include "CreateElementTxn.h" -#include "mozilla/dom/Element.h" #include "nsAlgorithm.h" +#include "nsAString.h" #include "nsDebug.h" #include "nsEditor.h" #include "nsError.h" #include "nsIContent.h" #include "nsIDOMCharacterData.h" #include "nsIEditor.h" #include "nsINode.h" #include "nsISelection.h" #include "nsISupportsUtils.h" #include "nsMemory.h" #include "nsReadableUtils.h" #include "nsStringFwd.h" #include "nsString.h" -#include "nsAString.h" -#include <algorithm> using namespace mozilla; using namespace mozilla::dom; -CreateElementTxn::CreateElementTxn() +CreateElementTxn::CreateElementTxn(nsEditor& aEditor, + nsIAtom& aTag, + nsINode& aParent, + int32_t aOffsetInParent) : EditTxn() + , mEditor(&aEditor) + , mTag(&aTag) + , mParent(&aParent) + , mOffsetInParent(aOffsetInParent) { } CreateElementTxn::~CreateElementTxn() { } NS_IMPL_CYCLE_COLLECTION_INHERITED(CreateElementTxn, EditTxn, mParent, mNewNode, mRefNode) NS_IMPL_ADDREF_INHERITED(CreateElementTxn, EditTxn) NS_IMPL_RELEASE_INHERITED(CreateElementTxn, EditTxn) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CreateElementTxn) NS_INTERFACE_MAP_END_INHERITING(EditTxn) -NS_IMETHODIMP CreateElementTxn::Init(nsEditor *aEditor, - const nsAString &aTag, - nsIDOMNode *aParent, - uint32_t aOffsetInParent) -{ - NS_ASSERTION(aEditor&&aParent, "null args"); - if (!aEditor || !aParent) { return NS_ERROR_NULL_POINTER; } - - mEditor = aEditor; - mTag = aTag; - mParent = do_QueryInterface(aParent); - mOffsetInParent = aOffsetInParent; - return NS_OK; -} -NS_IMETHODIMP CreateElementTxn::DoTransaction(void) +NS_IMETHODIMP +CreateElementTxn::DoTransaction() { - NS_ASSERTION(mEditor && mParent, "bad state"); - NS_ENSURE_TRUE(mEditor && mParent, NS_ERROR_NOT_INITIALIZED); + MOZ_ASSERT(mEditor && mTag && mParent); - nsCOMPtr<Element> newContent = - mEditor->CreateHTMLContent(nsCOMPtr<nsIAtom>(do_GetAtom(mTag))); - NS_ENSURE_STATE(newContent); + mNewNode = mEditor->CreateHTMLContent(mTag); + NS_ENSURE_STATE(mNewNode); - mNewNode = newContent->AsDOMNode(); // Try to insert formatting whitespace for the new node: - mEditor->MarkNodeDirty(mNewNode); + mEditor->MarkNodeDirty(GetAsDOMNode(mNewNode)); - // insert the new node - if (CreateElementTxn::eAppend == int32_t(mOffsetInParent)) { - nsCOMPtr<nsIDOMNode> resultNode; - return mParent->AppendChild(mNewNode, getter_AddRefs(resultNode)); + // Insert the new node + ErrorResult rv; + if (mOffsetInParent == -1) { + mParent->AppendChild(*mNewNode, rv); + return rv.ErrorCode(); } - nsCOMPtr<nsINode> parent = do_QueryInterface(mParent); - NS_ENSURE_STATE(parent); + mOffsetInParent = std::min(mOffsetInParent, + static_cast<int32_t>(mParent->GetChildCount())); - mOffsetInParent = std::min(mOffsetInParent, parent->GetChildCount()); - - // note, it's ok for mRefNode to be null. that means append - nsIContent* refNode = parent->GetChildAt(mOffsetInParent); - mRefNode = refNode ? refNode->AsDOMNode() : nullptr; + // Note, it's ok for mRefNode to be null. That means append + mRefNode = mParent->GetChildAt(mOffsetInParent); - nsCOMPtr<nsIDOMNode> resultNode; - nsresult result = mParent->InsertBefore(mNewNode, mRefNode, getter_AddRefs(resultNode)); - NS_ENSURE_SUCCESS(result, result); + mParent->InsertBefore(*mNewNode, mRefNode, rv); + NS_ENSURE_SUCCESS(rv.ErrorCode(), rv.ErrorCode()); - // only set selection to insertion point if editor gives permission - bool bAdjustSelection; - mEditor->ShouldTxnSetSelection(&bAdjustSelection); - if (!bAdjustSelection) { - // do nothing - dom range gravity will adjust selection + // Only set selection to insertion point if editor gives permission + if (!mEditor->GetShouldTxnSetSelection()) { + // Do nothing - DOM range gravity will adjust selection return NS_OK; } - nsCOMPtr<nsISelection> selection; - result = mEditor->GetSelection(getter_AddRefs(selection)); - NS_ENSURE_SUCCESS(result, result); + nsRefPtr<Selection> selection = mEditor->GetSelection(); NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); - nsCOMPtr<nsIContent> parentContent = do_QueryInterface(mParent); - NS_ENSURE_STATE(parentContent); - - result = selection->CollapseNative(parentContent, - parentContent->IndexOf(newContent) + 1); - NS_ASSERTION((NS_SUCCEEDED(result)), "selection could not be collapsed after insert."); - return result; -} - -NS_IMETHODIMP CreateElementTxn::UndoTransaction(void) -{ - NS_ASSERTION(mEditor && mParent, "bad state"); - NS_ENSURE_TRUE(mEditor && mParent, NS_ERROR_NOT_INITIALIZED); - - nsCOMPtr<nsIDOMNode> resultNode; - return mParent->RemoveChild(mNewNode, getter_AddRefs(resultNode)); -} - -NS_IMETHODIMP CreateElementTxn::RedoTransaction(void) -{ - NS_ASSERTION(mEditor && mParent, "bad state"); - NS_ENSURE_TRUE(mEditor && mParent, NS_ERROR_NOT_INITIALIZED); - - // first, reset mNewNode so it has no attributes or content - nsCOMPtr<nsIDOMCharacterData>nodeAsText = do_QueryInterface(mNewNode); - if (nodeAsText) - { - nodeAsText->SetData(EmptyString()); - } - - // now, reinsert mNewNode - nsCOMPtr<nsIDOMNode> resultNode; - return mParent->InsertBefore(mNewNode, mRefNode, getter_AddRefs(resultNode)); -} - -NS_IMETHODIMP CreateElementTxn::GetTxnDescription(nsAString& aString) -{ - aString.AssignLiteral("CreateElementTxn: "); - aString += mTag; + rv = selection->CollapseNative(mParent, mParent->IndexOf(mNewNode) + 1); + NS_ASSERTION(!rv.Failed(), + "selection could not be collapsed after insert"); return NS_OK; } -NS_IMETHODIMP CreateElementTxn::GetNewNode(nsIDOMNode **aNewNode) +NS_IMETHODIMP +CreateElementTxn::UndoTransaction() +{ + MOZ_ASSERT(mEditor && mParent); + + ErrorResult rv; + mParent->RemoveChild(*mNewNode, rv); + + return rv.ErrorCode(); +} + +NS_IMETHODIMP +CreateElementTxn::RedoTransaction() { - NS_ENSURE_TRUE(aNewNode, NS_ERROR_NULL_POINTER); - NS_ENSURE_TRUE(mNewNode, NS_ERROR_NOT_INITIALIZED); - *aNewNode = mNewNode; - NS_ADDREF(*aNewNode); + MOZ_ASSERT(mEditor && mParent); + + // First, reset mNewNode so it has no attributes or content + // XXX We never actually did this, we only cleared mNewNode's contents if it + // was a CharacterData node (which it's not, it's an Element) + + // Now, reinsert mNewNode + ErrorResult rv; + mParent->InsertBefore(*mNewNode, mRefNode, rv); + return rv.ErrorCode(); +} + +NS_IMETHODIMP +CreateElementTxn::GetTxnDescription(nsAString& aString) +{ + aString.AssignLiteral("CreateElementTxn: "); + aString += nsDependentAtomString(mTag); return NS_OK; } + +already_AddRefed<Element> +CreateElementTxn::GetNewNode() +{ + return nsCOMPtr<Element>(mNewNode).forget(); +}
--- a/editor/libeditor/CreateElementTxn.h +++ b/editor/libeditor/CreateElementTxn.h @@ -4,69 +4,73 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef CreateElementTxn_h__ #define CreateElementTxn_h__ #include "EditTxn.h" #include "nsCOMPtr.h" #include "nsCycleCollectionParticipant.h" -#include "nsIDOMNode.h" #include "nsISupportsImpl.h" -#include "nsString.h" -#include "nscore.h" class nsEditor; +class nsIAtom; +class nsIContent; +class nsINode; /** * A transaction that creates a new node in the content tree. */ +namespace mozilla { +namespace dom { + +class Element; + class CreateElementTxn : public EditTxn { public: - enum { eAppend=-1 }; - /** Initialize the transaction. * @param aEditor the provider of basic editing functionality * @param aTag the tag (P, HR, TABLE, etc.) for the new element * @param aParent the node into which the new element will be inserted * @param aOffsetInParent the location in aParent to insert the new element * if eAppend, the new element is appended as the last child */ - NS_IMETHOD Init(nsEditor *aEditor, - const nsAString& aTag, - nsIDOMNode *aParent, - uint32_t aOffsetInParent); - - CreateElementTxn(); + CreateElementTxn(nsEditor& aEditor, + nsIAtom& aTag, + nsINode& aParent, + int32_t aOffsetInParent); NS_DECL_ISUPPORTS_INHERITED NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(CreateElementTxn, EditTxn) NS_DECL_EDITTXN NS_IMETHOD RedoTransaction(); - NS_IMETHOD GetNewNode(nsIDOMNode **aNewNode); + already_AddRefed<Element> GetNewNode(); protected: virtual ~CreateElementTxn(); /** the document into which the new node will be inserted */ nsEditor* mEditor; - + /** the tag (mapping to object type) for the new element */ - nsString mTag; + nsCOMPtr<nsIAtom> mTag; /** the node into which the new node will be inserted */ - nsCOMPtr<nsIDOMNode> mParent; + nsCOMPtr<nsINode> mParent; /** the index in mParent for the new node */ - uint32_t mOffsetInParent; + int32_t mOffsetInParent; /** the new node to insert */ - nsCOMPtr<nsIDOMNode> mNewNode; + nsCOMPtr<Element> mNewNode; /** the node we will insert mNewNode before. We compute this ourselves. */ - nsCOMPtr<nsIDOMNode> mRefNode; + nsCOMPtr<nsIContent> mRefNode; }; +} +} + #endif
--- a/editor/libeditor/DeleteRangeTxn.cpp +++ b/editor/libeditor/DeleteRangeTxn.cpp @@ -134,28 +134,31 @@ DeleteRangeTxn::GetTxnDescription(nsAStr nsresult DeleteRangeTxn::CreateTxnsToDeleteBetween(nsINode* aNode, int32_t aStartOffset, int32_t aEndOffset) { // see what kind of node we have if (aNode->IsNodeOfType(nsINode::eDATA_NODE)) { // if the node is a chardata node, then delete chardata content - nsRefPtr<DeleteTextTxn> txn = new DeleteTextTxn(); - int32_t numToDel; if (aStartOffset == aEndOffset) { numToDel = 1; } else { numToDel = aEndOffset - aStartOffset; } - nsCOMPtr<nsIDOMCharacterData> charDataNode = do_QueryInterface(aNode); - nsresult res = txn->Init(mEditor, charDataNode, aStartOffset, numToDel, - mRangeUpdater); + nsRefPtr<nsGenericDOMDataNode> charDataNode = + static_cast<nsGenericDOMDataNode*>(aNode); + + nsRefPtr<DeleteTextTxn> txn = + new DeleteTextTxn(*mEditor, *charDataNode, aStartOffset, numToDel, + mRangeUpdater); + + nsresult res = txn->Init(); NS_ENSURE_SUCCESS(res, res); AppendChild(txn); return NS_OK; } nsCOMPtr<nsIContent> child = aNode->GetChildAt(aStartOffset); NS_ENSURE_STATE(child); @@ -188,21 +191,22 @@ DeleteRangeTxn::CreateTxnsToDeleteConten start = aOffset; numToDelete = aNode->Length() - aOffset; } else { start = 0; numToDelete = aOffset; } if (numToDelete) { - nsRefPtr<DeleteTextTxn> txn = new DeleteTextTxn(); + nsRefPtr<nsGenericDOMDataNode> dataNode = + static_cast<nsGenericDOMDataNode*>(aNode); + nsRefPtr<DeleteTextTxn> txn = new DeleteTextTxn(*mEditor, *dataNode, + start, numToDelete, mRangeUpdater); - nsCOMPtr<nsIDOMCharacterData> charDataNode = do_QueryInterface(aNode); - nsresult res = txn->Init(mEditor, charDataNode, start, numToDelete, - mRangeUpdater); + nsresult res = txn->Init(); NS_ENSURE_SUCCESS(res, res); AppendChild(txn); } } return NS_OK; }
--- a/editor/libeditor/DeleteTextTxn.cpp +++ b/editor/libeditor/DeleteTextTxn.cpp @@ -14,99 +14,83 @@ #include "nsISelection.h" #include "nsISupportsImpl.h" #include "nsSelectionState.h" #include "nsAString.h" using namespace mozilla; using namespace mozilla::dom; -DeleteTextTxn::DeleteTextTxn() : - EditTxn(), - mEditor(nullptr), - mCharData(), - mOffset(0), - mNumCharsToDelete(0), - mRangeUpdater(nullptr) +DeleteTextTxn::DeleteTextTxn(nsEditor& aEditor, + nsGenericDOMDataNode& aCharData, uint32_t aOffset, + uint32_t aNumCharsToDelete, + nsRangeUpdater* aRangeUpdater) + : EditTxn() + , mEditor(aEditor) + , mCharData(&aCharData) + , mOffset(aOffset) + , mNumCharsToDelete(aNumCharsToDelete) + , mRangeUpdater(aRangeUpdater) { + NS_ASSERTION(mCharData->Length() >= aOffset + aNumCharsToDelete, + "Trying to delete more characters than in node"); } NS_IMPL_CYCLE_COLLECTION_INHERITED(DeleteTextTxn, EditTxn, mCharData) NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(DeleteTextTxn) NS_INTERFACE_MAP_END_INHERITING(EditTxn) -NS_IMETHODIMP -DeleteTextTxn::Init(nsEditor* aEditor, - nsIDOMCharacterData* aCharData, - uint32_t aOffset, - uint32_t aNumCharsToDelete, - nsRangeUpdater* aRangeUpdater) +nsresult +DeleteTextTxn::Init() { - MOZ_ASSERT(aEditor && aCharData); - - mEditor = aEditor; - mCharData = aCharData; - - // do nothing if the node is read-only - if (!mEditor->IsModifiableNode(mCharData)) { + // Do nothing if the node is read-only + if (!mEditor.IsModifiableNode(mCharData)) { return NS_ERROR_FAILURE; } - mOffset = aOffset; - mNumCharsToDelete = aNumCharsToDelete; -#ifdef DEBUG - uint32_t length; - mCharData->GetLength(&length); - NS_ASSERTION(length >= aOffset + aNumCharsToDelete, - "Trying to delete more characters than in node"); -#endif - mDeletedText.Truncate(); - mRangeUpdater = aRangeUpdater; return NS_OK; } NS_IMETHODIMP DeleteTextTxn::DoTransaction() { - MOZ_ASSERT(mEditor && mCharData); + MOZ_ASSERT(mCharData); - // get the text that we're about to delete + // Get the text that we're about to delete nsresult res = mCharData->SubstringData(mOffset, mNumCharsToDelete, mDeletedText); MOZ_ASSERT(NS_SUCCEEDED(res)); res = mCharData->DeleteData(mOffset, mNumCharsToDelete); NS_ENSURE_SUCCESS(res, res); if (mRangeUpdater) { mRangeUpdater->SelAdjDeleteText(mCharData, mOffset, mNumCharsToDelete); } - // only set selection to deletion point if editor gives permission - bool bAdjustSelection; - mEditor->ShouldTxnSetSelection(&bAdjustSelection); - if (bAdjustSelection) { - nsRefPtr<Selection> selection = mEditor->GetSelection(); + // Only set selection to deletion point if editor gives permission + if (mEditor.GetShouldTxnSetSelection()) { + nsRefPtr<Selection> selection = mEditor.GetSelection(); NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); res = selection->Collapse(mCharData, mOffset); NS_ASSERTION(NS_SUCCEEDED(res), - "selection could not be collapsed after undo of deletetext."); + "Selection could not be collapsed after undo of deletetext"); NS_ENSURE_SUCCESS(res, res); } - // else do nothing - dom range gravity will adjust selection + // Else do nothing - DOM Range gravity will adjust selection return NS_OK; } -//XXX: we may want to store the selection state and restore it properly -// was it an insertion point or an extended selection? +//XXX: We may want to store the selection state and restore it properly. Was +// it an insertion point or an extended selection? NS_IMETHODIMP DeleteTextTxn::UndoTransaction() { - MOZ_ASSERT(mEditor && mCharData); + MOZ_ASSERT(mCharData); return mCharData->InsertData(mOffset, mDeletedText); } NS_IMETHODIMP DeleteTextTxn::GetTxnDescription(nsAString& aString) { aString.AssignLiteral("DeleteTextTxn: ");
--- a/editor/libeditor/DeleteTextTxn.h +++ b/editor/libeditor/DeleteTextTxn.h @@ -4,67 +4,73 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ #ifndef DeleteTextTxn_h__ #define DeleteTextTxn_h__ #include "EditTxn.h" #include "nsCOMPtr.h" #include "nsCycleCollectionParticipant.h" +#include "nsGenericDOMDataNode.h" #include "nsID.h" -#include "nsIDOMCharacterData.h" #include "nsString.h" #include "nscore.h" class nsEditor; class nsRangeUpdater; +namespace mozilla { +namespace dom { + /** * A transaction that removes text from a content node. */ class DeleteTextTxn : public EditTxn { public: /** initialize the transaction. * @param aEditor the provider of basic editing operations * @param aElement the content node to remove text from * @param aOffset the location in aElement to begin the deletion * @param aNumCharsToDelete the number of characters to delete. Not the number of bytes! */ - NS_IMETHOD Init(nsEditor* aEditor, - nsIDOMCharacterData* aCharData, - uint32_t aOffset, - uint32_t aNumCharsToDelete, - nsRangeUpdater* aRangeUpdater); + DeleteTextTxn(nsEditor& aEditor, + nsGenericDOMDataNode& aCharData, + uint32_t aOffset, + uint32_t aNumCharsToDelete, + nsRangeUpdater* aRangeUpdater); - DeleteTextTxn(); + nsresult Init(); NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(DeleteTextTxn, EditTxn) NS_IMETHOD QueryInterface(REFNSIID aIID, void** aInstancePtr); NS_DECL_EDITTXN uint32_t GetOffset() { return mOffset; } uint32_t GetNumCharsToDelete() { return mNumCharsToDelete; } protected: /** the provider of basic editing operations */ - nsEditor* mEditor; + nsEditor& mEditor; /** the CharacterData node to operate upon */ - nsCOMPtr<nsIDOMCharacterData> mCharData; + nsRefPtr<nsGenericDOMDataNode> mCharData; /** the offset into mCharData where the deletion is to take place */ uint32_t mOffset; /** the number of characters to delete */ uint32_t mNumCharsToDelete; /** the text that was deleted */ nsString mDeletedText; /** range updater object */ nsRangeUpdater* mRangeUpdater; }; +} +} + #endif
rename from editor/libeditor/InsertElementTxn.cpp rename to editor/libeditor/InsertNodeTxn.cpp --- a/editor/libeditor/InsertElementTxn.cpp +++ b/editor/libeditor/InsertNodeTxn.cpp @@ -1,112 +1,93 @@ /* -*- 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 <stdio.h> // for printf -#include "InsertElementTxn.h" +#include "mozilla/dom/Selection.h" // for Selection + +#include "InsertNodeTxn.h" #include "nsAString.h" #include "nsDebug.h" // for NS_ENSURE_TRUE, etc #include "nsEditor.h" // for nsEditor #include "nsError.h" // for NS_ERROR_NULL_POINTER, etc #include "nsIContent.h" // for nsIContent -#include "nsINode.h" // for nsINode -#include "nsISelection.h" // for nsISelection #include "nsMemory.h" // for nsMemory #include "nsReadableUtils.h" // for ToNewCString #include "nsString.h" // for nsString using namespace mozilla; +using namespace mozilla::dom; -InsertElementTxn::InsertElementTxn() +InsertNodeTxn::InsertNodeTxn(nsIContent& aNode, nsINode& aParent, + int32_t aOffset, nsEditor& aEditor) : EditTxn() + , mNode(&aNode) + , mParent(&aParent) + , mOffset(aOffset) + , mEditor(aEditor) { } -InsertElementTxn::~InsertElementTxn() +InsertNodeTxn::~InsertNodeTxn() { } -NS_IMPL_CYCLE_COLLECTION_INHERITED(InsertElementTxn, EditTxn, +NS_IMPL_CYCLE_COLLECTION_INHERITED(InsertNodeTxn, EditTxn, mNode, mParent) -NS_IMPL_ADDREF_INHERITED(InsertElementTxn, EditTxn) -NS_IMPL_RELEASE_INHERITED(InsertElementTxn, EditTxn) -NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(InsertElementTxn) +NS_IMPL_ADDREF_INHERITED(InsertNodeTxn, EditTxn) +NS_IMPL_RELEASE_INHERITED(InsertNodeTxn, EditTxn) +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(InsertNodeTxn) NS_INTERFACE_MAP_END_INHERITING(EditTxn) -NS_IMETHODIMP InsertElementTxn::Init(nsIDOMNode *aNode, - nsIDOMNode *aParent, - int32_t aOffset, - nsIEditor *aEditor) +NS_IMETHODIMP +InsertNodeTxn::DoTransaction() { - NS_ASSERTION(aNode && aParent && aEditor, "bad arg"); - NS_ENSURE_TRUE(aNode && aParent && aEditor, NS_ERROR_NULL_POINTER); + MOZ_ASSERT(mNode && mParent); - mNode = do_QueryInterface(aNode); - mParent = do_QueryInterface(aParent); - mOffset = aOffset; - mEditor = aEditor; - NS_ENSURE_TRUE(mNode && mParent && mEditor, NS_ERROR_INVALID_ARG); - return NS_OK; -} - - -NS_IMETHODIMP InsertElementTxn::DoTransaction(void) -{ - NS_ENSURE_TRUE(mNode && mParent, NS_ERROR_NOT_INITIALIZED); - - nsCOMPtr<nsINode> parent = do_QueryInterface(mParent); - NS_ENSURE_STATE(parent); - - uint32_t count = parent->GetChildCount(); - if (mOffset > int32_t(count) || mOffset == -1) { + uint32_t count = mParent->GetChildCount(); + if (mOffset > static_cast<int32_t>(count) || mOffset == -1) { // -1 is sentinel value meaning "append at end" mOffset = count; } - // note, it's ok for refContent to be null. that means append - nsCOMPtr<nsIContent> refContent = parent->GetChildAt(mOffset); - nsCOMPtr<nsIDOMNode> refNode = refContent ? refContent->AsDOMNode() : nullptr; + // Note, it's ok for ref to be null. That means append. + nsCOMPtr<nsIContent> ref = mParent->GetChildAt(mOffset); - mEditor->MarkNodeDirty(mNode); + mEditor.MarkNodeDirty(GetAsDOMNode(mNode)); - nsCOMPtr<nsIDOMNode> resultNode; - nsresult result = mParent->InsertBefore(mNode, refNode, getter_AddRefs(resultNode)); - NS_ENSURE_SUCCESS(result, result); - NS_ENSURE_TRUE(resultNode, NS_ERROR_NULL_POINTER); + ErrorResult rv; + mParent->InsertBefore(*mNode, ref, rv); + NS_ENSURE_SUCCESS(rv.ErrorCode(), rv.ErrorCode()); - // only set selection to insertion point if editor gives permission - bool bAdjustSelection; - mEditor->ShouldTxnSetSelection(&bAdjustSelection); - if (bAdjustSelection) - { - nsCOMPtr<nsISelection> selection; - result = mEditor->GetSelection(getter_AddRefs(selection)); - NS_ENSURE_SUCCESS(result, result); + // Only set selection to insertion point if editor gives permission + if (mEditor.GetShouldTxnSetSelection()) { + nsRefPtr<Selection> selection = mEditor.GetSelection(); NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); - // place the selection just after the inserted element - selection->Collapse(mParent, mOffset+1); + // Place the selection just after the inserted element + selection->Collapse(mParent, mOffset + 1); + } else { + // Do nothing - DOM Range gravity will adjust selection } - else - { - // do nothing - dom range gravity will adjust selection - } - return result; + return NS_OK; } -NS_IMETHODIMP InsertElementTxn::UndoTransaction(void) +NS_IMETHODIMP +InsertNodeTxn::UndoTransaction() { - NS_ENSURE_TRUE(mNode && mParent, NS_ERROR_NOT_INITIALIZED); + MOZ_ASSERT(mNode && mParent); - nsCOMPtr<nsIDOMNode> resultNode; - return mParent->RemoveChild(mNode, getter_AddRefs(resultNode)); + ErrorResult rv; + mParent->RemoveChild(*mNode, rv); + return rv.ErrorCode(); } -NS_IMETHODIMP InsertElementTxn::GetTxnDescription(nsAString& aString) +NS_IMETHODIMP +InsertNodeTxn::GetTxnDescription(nsAString& aString) { - aString.AssignLiteral("InsertElementTxn"); + aString.AssignLiteral("InsertNodeTxn"); return NS_OK; }
rename from editor/libeditor/InsertElementTxn.h rename to editor/libeditor/InsertNodeTxn.h --- a/editor/libeditor/InsertElementTxn.h +++ b/editor/libeditor/InsertNodeTxn.h @@ -1,57 +1,58 @@ /* -*- 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/. */ -#ifndef InsertElementTxn_h__ -#define InsertElementTxn_h__ +#ifndef InsertNodeTxn_h__ +#define InsertNodeTxn_h__ #include "EditTxn.h" // for EditTxn, NS_DECL_EDITTXN #include "nsCOMPtr.h" // for nsCOMPtr #include "nsCycleCollectionParticipant.h" -#include "nsIDOMNode.h" // for nsIDOMNode +#include "nsIContent.h" // for nsIContent #include "nsISupportsImpl.h" // for NS_DECL_ISUPPORTS_INHERITED -#include "nscore.h" // for NS_IMETHOD + +class nsEditor; -class nsIEditor; +namespace mozilla { +namespace dom { /** * A transaction that inserts a single element */ -class InsertElementTxn : public EditTxn +class InsertNodeTxn : public EditTxn { public: /** initialize the transaction. * @param aNode the node to insert * @param aParent the node to insert into * @param aOffset the offset in aParent to insert aNode */ - NS_IMETHOD Init(nsIDOMNode *aNode, - nsIDOMNode *aParent, - int32_t aOffset, - nsIEditor *aEditor); - - InsertElementTxn(); + InsertNodeTxn(nsIContent& aNode, nsINode& aParent, int32_t aOffset, + nsEditor& aEditor); NS_DECL_ISUPPORTS_INHERITED - NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(InsertElementTxn, EditTxn) + NS_DECL_CYCLE_COLLECTION_CLASS_INHERITED(InsertNodeTxn, EditTxn) NS_DECL_EDITTXN protected: - virtual ~InsertElementTxn(); + virtual ~InsertNodeTxn(); /** the element to insert */ - nsCOMPtr<nsIDOMNode> mNode; + nsCOMPtr<nsIContent> mNode; /** the node into which the new node will be inserted */ - nsCOMPtr<nsIDOMNode> mParent; - - /** the editor for this transaction */ - nsIEditor* mEditor; + nsCOMPtr<nsINode> mParent; /** the index in mParent for the new node */ int32_t mOffset; + + /** the editor for this transaction */ + nsEditor& mEditor; }; +} +} + #endif
--- a/editor/libeditor/moz.build +++ b/editor/libeditor/moz.build @@ -11,17 +11,17 @@ UNIFIED_SOURCES += [ 'ChangeCSSInlineStyleTxn.cpp', 'CreateElementTxn.cpp', 'DeleteNodeTxn.cpp', 'DeleteRangeTxn.cpp', 'DeleteTextTxn.cpp', 'EditAggregateTxn.cpp', 'EditTxn.cpp', 'IMETextTxn.cpp', - 'InsertElementTxn.cpp', + 'InsertNodeTxn.cpp', 'InsertTextTxn.cpp', 'JoinElementTxn.cpp', 'nsEditor.cpp', 'nsEditorCommands.cpp', 'nsEditorController.cpp', 'nsEditorEventListener.cpp', 'nsEditorUtils.cpp', 'nsEditProperty.cpp',
--- a/editor/libeditor/nsEditor.cpp +++ b/editor/libeditor/nsEditor.cpp @@ -11,17 +11,17 @@ #include "ChangeAttributeTxn.h" // for ChangeAttributeTxn #include "CreateElementTxn.h" // for CreateElementTxn #include "DeleteNodeTxn.h" // for DeleteNodeTxn #include "DeleteRangeTxn.h" // for DeleteRangeTxn #include "DeleteTextTxn.h" // for DeleteTextTxn #include "EditAggregateTxn.h" // for EditAggregateTxn #include "EditTxn.h" // for EditTxn #include "IMETextTxn.h" // for IMETextTxn -#include "InsertElementTxn.h" // for InsertElementTxn +#include "InsertNodeTxn.h" // for InsertNodeTxn #include "InsertTextTxn.h" // for InsertTextTxn #include "JoinElementTxn.h" // for JoinElementTxn #include "PlaceholderTxn.h" // for PlaceholderTxn #include "SplitElementTxn.h" // for SplitElementTxn #include "mozFlushType.h" // for mozFlushType::Flush_Frames #include "mozISpellCheckingEngine.h" #include "mozInlineSpellChecker.h" // for mozInlineSpellChecker #include "mozilla/IMEStateManager.h" // for IMEStateManager @@ -1330,82 +1330,99 @@ NS_IMETHODIMP nsEditor::SyncRealTimeSpel NS_IMETHODIMP nsEditor::SetSpellcheckUserOverride(bool enable) { mSpellcheckCheckboxState = enable ? eTriTrue : eTriFalse; return SyncRealTimeSpell(); } -NS_IMETHODIMP nsEditor::CreateNode(const nsAString& aTag, - nsIDOMNode * aParent, - int32_t aPosition, - nsIDOMNode ** aNewNode) -{ - int32_t i; +NS_IMETHODIMP +nsEditor::CreateNode(const nsAString& aTag, + nsIDOMNode* aParent, + int32_t aPosition, + nsIDOMNode** aNewNode) +{ + nsCOMPtr<nsIAtom> tag = do_GetAtom(aTag); + nsCOMPtr<nsINode> parent = do_QueryInterface(aParent); + NS_ENSURE_STATE(parent); + *aNewNode = GetAsDOMNode(CreateNode(tag, parent, aPosition).take()); + NS_ENSURE_STATE(*aNewNode); + return NS_OK; +} + +already_AddRefed<Element> +nsEditor::CreateNode(nsIAtom* aTag, + nsINode* aParent, + int32_t aPosition) +{ + MOZ_ASSERT(aTag && aParent); nsAutoRules beginRulesSniffing(this, EditAction::createNode, nsIEditor::eNext); - for (i = 0; i < mActionListeners.Count(); i++) - mActionListeners[i]->WillCreateNode(aTag, aParent, aPosition); - - nsRefPtr<CreateElementTxn> txn; - nsresult result = CreateTxnForCreateElement(aTag, aParent, aPosition, - getter_AddRefs(txn)); - if (NS_SUCCEEDED(result)) - { - result = DoTransaction(txn); - if (NS_SUCCEEDED(result)) - { - result = txn->GetNewNode(aNewNode); - NS_ASSERTION((NS_SUCCEEDED(result)), "GetNewNode can't fail if txn::DoTransaction succeeded."); - } + for (int32_t i = 0; i < mActionListeners.Count(); i++) { + mActionListeners[i]->WillCreateNode(nsDependentAtomString(aTag), + GetAsDOMNode(aParent), aPosition); + } + + nsCOMPtr<Element> ret; + + nsRefPtr<CreateElementTxn> txn = + CreateTxnForCreateElement(*aTag, *aParent, aPosition); + nsresult res = DoTransaction(txn); + if (NS_SUCCEEDED(res)) { + ret = txn->GetNewNode(); + MOZ_ASSERT(ret); } mRangeUpdater.SelAdjCreateNode(aParent, aPosition); - for (i = 0; i < mActionListeners.Count(); i++) - mActionListeners[i]->DidCreateNode(aTag, *aNewNode, aParent, aPosition, result); - - return result; -} - + for (int32_t i = 0; i < mActionListeners.Count(); i++) { + mActionListeners[i]->DidCreateNode(nsDependentAtomString(aTag), + GetAsDOMNode(ret), + GetAsDOMNode(aParent), aPosition, + res); + } + + return ret.forget(); +} + + +NS_IMETHODIMP +nsEditor::InsertNode(nsIDOMNode* aNode, nsIDOMNode* aParent, int32_t aPosition) +{ + nsCOMPtr<nsIContent> node = do_QueryInterface(aNode); + nsCOMPtr<nsINode> parent = do_QueryInterface(aParent); + NS_ENSURE_TRUE(node && parent, NS_ERROR_NULL_POINTER); + + return InsertNode(*node, *parent, aPosition); +} nsresult -nsEditor::InsertNode(nsIContent* aContent, nsINode* aParent, int32_t aPosition) -{ - MOZ_ASSERT(aContent && aParent); - return InsertNode(GetAsDOMNode(aContent), GetAsDOMNode(aParent), aPosition); -} - -NS_IMETHODIMP nsEditor::InsertNode(nsIDOMNode * aNode, - nsIDOMNode * aParent, - int32_t aPosition) -{ - int32_t i; +nsEditor::InsertNode(nsIContent& aNode, nsINode& aParent, int32_t aPosition) +{ nsAutoRules beginRulesSniffing(this, EditAction::insertNode, nsIEditor::eNext); - for (i = 0; i < mActionListeners.Count(); i++) - mActionListeners[i]->WillInsertNode(aNode, aParent, aPosition); - - nsRefPtr<InsertElementTxn> txn; - nsCOMPtr<nsINode> node = do_QueryInterface(aNode); - nsCOMPtr<nsINode> parent = do_QueryInterface(aParent); - nsresult result = CreateTxnForInsertElement(node->AsDOMNode(), parent->AsDOMNode(), - aPosition, getter_AddRefs(txn)); - if (NS_SUCCEEDED(result)) { - result = DoTransaction(txn); - } - - mRangeUpdater.SelAdjInsertNode(aParent, aPosition); - - for (i = 0; i < mActionListeners.Count(); i++) - mActionListeners[i]->DidInsertNode(aNode, aParent, aPosition, result); - - return result; + for (int32_t i = 0; i < mActionListeners.Count(); i++) { + mActionListeners[i]->WillInsertNode(aNode.AsDOMNode(), aParent.AsDOMNode(), + aPosition); + } + + nsRefPtr<InsertNodeTxn> txn = CreateTxnForInsertNode(aNode, aParent, + aPosition); + nsresult res = DoTransaction(txn); + + mRangeUpdater.SelAdjInsertNode(aParent.AsDOMNode(), aPosition); + + for (int32_t i = 0; i < mActionListeners.Count(); i++) { + mActionListeners[i]->DidInsertNode(aNode.AsDOMNode(), aParent.AsDOMNode(), + aPosition, res); + } + + return res; } NS_IMETHODIMP nsEditor::SplitNode(nsIDOMNode * aNode, int32_t aOffset, nsIDOMNode **aNewLeftNode) { @@ -1550,207 +1567,165 @@ nsEditor::ReplaceContainer(Element* aOld } if (aCloneAttributes == eCloneAttributes) { CloneAttributes(ret, aOldContainer); } // notify our internal selection state listener // (Note: A nsAutoSelectionReset object must be created // before calling this to initialize mRangeUpdater) - nsAutoReplaceContainerSelNotify selStateNotify(mRangeUpdater, - aOldContainer->AsDOMNode(), ret->AsDOMNode()); + AutoReplaceContainerSelNotify selStateNotify(mRangeUpdater, aOldContainer, + ret); { nsAutoTxnsConserveSelection conserveSelection(this); while (aOldContainer->HasChildren()) { nsCOMPtr<nsIContent> child = aOldContainer->GetFirstChild(); res = DeleteNode(child); NS_ENSURE_SUCCESS(res, nullptr); - res = InsertNode(child, ret, -1); + res = InsertNode(*child, *ret, -1); NS_ENSURE_SUCCESS(res, nullptr); } } // insert new container into tree - res = InsertNode(ret, parent, offset); + res = InsertNode(*ret, *parent, offset); NS_ENSURE_SUCCESS(res, nullptr); // delete old container res = DeleteNode(aOldContainer); NS_ENSURE_SUCCESS(res, nullptr); return ret.forget(); } -/////////////////////////////////////////////////////////////////////////// -// RemoveContainer: remove inNode, reparenting its children into their -// the parent of inNode +/////////////////////////////////////////////////////////////////////////////// +// RemoveContainer: remove inNode, reparenting its children (if any) into the +// parent of inNode // nsresult -nsEditor::RemoveContainer(nsIDOMNode* aNode) -{ - nsCOMPtr<nsINode> node = do_QueryInterface(aNode); - return RemoveContainer(node); -} - -nsresult -nsEditor::RemoveContainer(nsINode* aNode) -{ - NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER); +nsEditor::RemoveContainer(nsIContent* aNode) +{ + MOZ_ASSERT(aNode); nsCOMPtr<nsINode> parent = aNode->GetParentNode(); NS_ENSURE_STATE(parent); int32_t offset = parent->IndexOf(aNode); - - // loop through the child nodes of inNode and promote them - // into inNode's parent. + + // Loop through the children of inNode and promote them into inNode's parent uint32_t nodeOrigLen = aNode->GetChildCount(); // notify our internal selection state listener - nsAutoRemoveContainerSelNotify selNotify(mRangeUpdater, aNode, parent, offset, nodeOrigLen); - + nsAutoRemoveContainerSelNotify selNotify(mRangeUpdater, aNode, parent, + offset, nodeOrigLen); + while (aNode->HasChildren()) { nsCOMPtr<nsIContent> child = aNode->GetLastChild(); - nsresult rv = DeleteNode(child->AsDOMNode()); + nsresult rv = DeleteNode(child); NS_ENSURE_SUCCESS(rv, rv); - rv = InsertNode(child->AsDOMNode(), parent->AsDOMNode(), offset); + rv = InsertNode(*child, *parent, offset); NS_ENSURE_SUCCESS(rv, rv); } - return DeleteNode(aNode->AsDOMNode()); -} - - -/////////////////////////////////////////////////////////////////////////// -// InsertContainerAbove: insert a new parent for inNode, returned in outNode, -// which is contructed to be of type aNodeType. outNode becomes -// a child of inNode's earlier parent. -// Callers responsibility to make sure inNode's can be child -// of outNode, and outNode can be child of old parent. -nsresult -nsEditor::InsertContainerAbove( nsIDOMNode *inNode, - nsCOMPtr<nsIDOMNode> *outNode, - const nsAString &aNodeType, - const nsAString *aAttribute, - const nsAString *aValue) -{ - NS_ENSURE_TRUE(inNode && outNode, NS_ERROR_NULL_POINTER); - - nsCOMPtr<nsIContent> node = do_QueryInterface(inNode); - NS_ENSURE_STATE(node); - - nsCOMPtr<dom::Element> element; - nsresult rv = InsertContainerAbove(node, getter_AddRefs(element), aNodeType, - aAttribute, aValue); - *outNode = element ? element->AsDOMNode() : nullptr; - return rv; -} - -nsresult + return DeleteNode(aNode); +} + + +/////////////////////////////////////////////////////////////////////////////// +// InsertContainerAbove: Insert a new parent for inNode, which is contructed to +// be of type aNodeType. outNode becomes a child of +// inNode's earlier parent. Caller's responsibility to +// make sure inNode's can be child of outNode, and +// outNode can be child of old parent. +already_AddRefed<Element> nsEditor::InsertContainerAbove(nsIContent* aNode, - dom::Element** aOutNode, - const nsAString& aNodeType, - const nsAString* aAttribute, + nsIAtom* aNodeType, + nsIAtom* aAttribute, const nsAString* aValue) { - MOZ_ASSERT(aNode); + MOZ_ASSERT(aNode && aNodeType); nsCOMPtr<nsIContent> parent = aNode->GetParent(); - NS_ENSURE_STATE(parent); + NS_ENSURE_TRUE(parent, nullptr); int32_t offset = parent->IndexOf(aNode); - // create new container - nsCOMPtr<Element> newContent = - CreateHTMLContent(nsCOMPtr<nsIAtom>(do_GetAtom(aNodeType))); - NS_ENSURE_STATE(newContent); - - // set attribute if needed + // Create new container + nsCOMPtr<Element> newContent = CreateHTMLContent(aNodeType); + NS_ENSURE_TRUE(newContent, nullptr); + + // Set attribute if needed nsresult res; - if (aAttribute && aValue && !aAttribute->IsEmpty()) { - nsIDOMNode* elem = newContent->AsDOMNode(); - res = static_cast<nsIDOMElement*>(elem)->SetAttribute(*aAttribute, *aValue); - NS_ENSURE_SUCCESS(res, res); - } - - // notify our internal selection state listener + if (aAttribute && aValue && aAttribute != nsGkAtoms::_empty) { + res = newContent->SetAttr(kNameSpaceID_None, aAttribute, *aValue, true); + NS_ENSURE_SUCCESS(res, nullptr); + } + + // Notify our internal selection state listener nsAutoInsertContainerSelNotify selNotify(mRangeUpdater); - - // put inNode in new parent, outNode - res = DeleteNode(aNode->AsDOMNode()); - NS_ENSURE_SUCCESS(res, res); + + // Put inNode in new parent, outNode + res = DeleteNode(aNode); + NS_ENSURE_SUCCESS(res, nullptr); { nsAutoTxnsConserveSelection conserveSelection(this); - res = InsertNode(aNode->AsDOMNode(), newContent->AsDOMNode(), 0); - NS_ENSURE_SUCCESS(res, res); - } - - // put new parent in doc - res = InsertNode(newContent->AsDOMNode(), parent->AsDOMNode(), offset); - newContent.forget(aOutNode); - return res; + res = InsertNode(*aNode, *newContent, 0); + NS_ENSURE_SUCCESS(res, nullptr); + } + + // Put new parent in doc + res = InsertNode(*newContent, *parent, offset); + NS_ENSURE_SUCCESS(res, nullptr); + + return newContent.forget(); } /////////////////////////////////////////////////////////////////////////// // MoveNode: move aNode to {aParent,aOffset} nsresult -nsEditor::MoveNode(nsIDOMNode* aNode, nsIDOMNode* aParent, int32_t aOffset) -{ - nsCOMPtr<nsINode> node = do_QueryInterface(aNode); - NS_ENSURE_STATE(node); - - nsCOMPtr<nsINode> parent = do_QueryInterface(aParent); - NS_ENSURE_STATE(parent); - - return MoveNode(node, parent, aOffset); -} - -nsresult -nsEditor::MoveNode(nsINode* aNode, nsINode* aParent, int32_t aOffset) +nsEditor::MoveNode(nsIContent* aNode, nsINode* aParent, int32_t aOffset) { MOZ_ASSERT(aNode); MOZ_ASSERT(aParent); MOZ_ASSERT(aOffset == -1 || (0 <= aOffset && SafeCast<uint32_t>(aOffset) <= aParent->Length())); - int32_t oldOffset; - nsCOMPtr<nsINode> oldParent = GetNodeLocation(aNode, &oldOffset); - + nsCOMPtr<nsINode> oldParent = aNode->GetParentNode(); + int32_t oldOffset = oldParent ? oldParent->IndexOf(aNode) : -1; + if (aOffset == -1) { - // Magic value meaning "move to end of aParent". + // Magic value meaning "move to end of aParent" aOffset = SafeCast<int32_t>(aParent->Length()); } - - // Don't do anything if it's already in right place. + + // Don't do anything if it's already in right place if (aParent == oldParent && aOffset == oldOffset) { return NS_OK; } - - // Notify our internal selection state listener. + + // Notify our internal selection state listener nsAutoMoveNodeSelNotify selNotify(mRangeUpdater, oldParent, oldOffset, aParent, aOffset); - - // Need to adjust aOffset if we are moving aNode further along in its current - // parent. + + // Need to adjust aOffset if we're moving aNode later in its current parent if (aParent == oldParent && oldOffset < aOffset) { - // This is because when we delete aNode, it will make the offsets after it - // off by one. + // When we delete aNode, it will make the offsets after it off by one aOffset--; } - // Hold a reference so aNode doesn't go away when we remove it (bug 772282). + // Hold a reference so aNode doesn't go away when we remove it (bug 772282) nsCOMPtr<nsINode> kungFuDeathGrip = aNode; nsresult rv = DeleteNode(aNode); NS_ENSURE_SUCCESS(rv, rv); - return InsertNode(aNode->AsDOMNode(), aParent->AsDOMNode(), aOffset); + return InsertNode(*aNode, *aParent, aOffset); } NS_IMETHODIMP nsEditor::AddEditorObserver(nsIEditorObserver *aObserver) { // we don't keep ownership of the observers. They must // remove themselves as observers before they are destroyed. @@ -2587,56 +2562,57 @@ NS_IMETHODIMP nsEditor::CreateTxnForInse { txn.forget(aTxn); } return rv; } -NS_IMETHODIMP nsEditor::DeleteText(nsIDOMCharacterData *aElement, - uint32_t aOffset, - uint32_t aLength) -{ - nsRefPtr<DeleteTextTxn> txn; - nsresult result = CreateTxnForDeleteText(aElement, aOffset, aLength, - getter_AddRefs(txn)); +nsresult +nsEditor::DeleteText(nsGenericDOMDataNode& aCharData, uint32_t aOffset, + uint32_t aLength) +{ + nsRefPtr<DeleteTextTxn> txn = + CreateTxnForDeleteText(aCharData, aOffset, aLength); + NS_ENSURE_STATE(txn); + nsAutoRules beginRulesSniffing(this, EditAction::deleteText, nsIEditor::ePrevious); - if (NS_SUCCEEDED(result)) - { - // let listeners know what's up - int32_t i; - for (i = 0; i < mActionListeners.Count(); i++) - mActionListeners[i]->WillDeleteText(aElement, aOffset, aLength); - - result = DoTransaction(txn); - - // let listeners know what happened - for (i = 0; i < mActionListeners.Count(); i++) - mActionListeners[i]->DidDeleteText(aElement, aOffset, aLength, result); - } - return result; -} - - -nsresult -nsEditor::CreateTxnForDeleteText(nsIDOMCharacterData* aElement, - uint32_t aOffset, - uint32_t aLength, - DeleteTextTxn** aTxn) -{ - NS_ENSURE_TRUE(aElement, NS_ERROR_NULL_POINTER); - - nsRefPtr<DeleteTextTxn> txn = new DeleteTextTxn(); - - nsresult res = txn->Init(this, aElement, aOffset, aLength, &mRangeUpdater); - NS_ENSURE_SUCCESS(res, res); - - txn.forget(aTxn); - return NS_OK; + + // Let listeners know what's up + for (int32_t i = 0; i < mActionListeners.Count(); i++) { + mActionListeners[i]->WillDeleteText( + static_cast<nsIDOMCharacterData*>(GetAsDOMNode(&aCharData)), aOffset, + aLength); + } + + nsresult res = DoTransaction(txn); + + // Let listeners know what happened + for (int32_t i = 0; i < mActionListeners.Count(); i++) { + mActionListeners[i]->DidDeleteText( + static_cast<nsIDOMCharacterData*>(GetAsDOMNode(&aCharData)), aOffset, + aLength, res); + } + + return res; +} + + +already_AddRefed<DeleteTextTxn> +nsEditor::CreateTxnForDeleteText(nsGenericDOMDataNode& aCharData, + uint32_t aOffset, uint32_t aLength) +{ + nsRefPtr<DeleteTextTxn> txn = + new DeleteTextTxn(*this, aCharData, aOffset, aLength, &mRangeUpdater); + + nsresult res = txn->Init(); + NS_ENSURE_SUCCESS(res, nullptr); + + return txn.forget(); } NS_IMETHODIMP nsEditor::CreateTxnForSplitNode(nsIDOMNode *aNode, uint32_t aOffset, SplitElementTxn **aTxn) @@ -4141,32 +4117,31 @@ nsEditor::DeleteSelectionImpl(EDirection return res; } // XXX: error handling in this routine needs to be cleaned up! NS_IMETHODIMP nsEditor::DeleteSelectionAndCreateNode(const nsAString& aTag, nsIDOMNode ** aNewNode) { + nsCOMPtr<nsIAtom> tag = do_GetAtom(aTag); + nsresult result = DeleteSelectionAndPrepareToCreateNode(); NS_ENSURE_SUCCESS(result, result); nsRefPtr<Selection> selection = GetSelection(); NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); nsCOMPtr<nsINode> node = selection->GetAnchorNode(); uint32_t offset = selection->AnchorOffset(); nsCOMPtr<nsIDOMNode> newNode; - result = CreateNode(aTag, node->AsDOMNode(), offset, - getter_AddRefs(newNode)); + *aNewNode = GetAsDOMNode(CreateNode(tag, node, offset).take()); // XXX: ERROR_HANDLING check result, and make sure aNewNode is set correctly // in success/failure cases - *aNewNode = newNode; - NS_IF_ADDREF(*aNewNode); // we want the selection to be just after the new node return selection->Collapse(node, offset + 1); } /* Non-interface, protected methods */ @@ -4312,51 +4287,36 @@ nsEditor::CreateTxnForRemoveAttribute(ns { txn.forget(aTxn); } return rv; } -NS_IMETHODIMP nsEditor::CreateTxnForCreateElement(const nsAString& aTag, - nsIDOMNode *aParent, - int32_t aPosition, - CreateElementTxn ** aTxn) -{ - NS_ENSURE_TRUE(aParent, NS_ERROR_NULL_POINTER); - - nsRefPtr<CreateElementTxn> txn = new CreateElementTxn(); - - nsresult rv = txn->Init(this, aTag, aParent, aPosition); - if (NS_SUCCEEDED(rv)) - { - txn.forget(aTxn); - } - - return rv; -} - - -NS_IMETHODIMP nsEditor::CreateTxnForInsertElement(nsIDOMNode * aNode, - nsIDOMNode * aParent, - int32_t aPosition, - InsertElementTxn ** aTxn) -{ - NS_ENSURE_TRUE(aNode && aParent, NS_ERROR_NULL_POINTER); - - nsRefPtr<InsertElementTxn> txn = new InsertElementTxn(); - - nsresult rv = txn->Init(aNode, aParent, aPosition, this); - if (NS_SUCCEEDED(rv)) - { - txn.forget(aTxn); - } - - return rv; +already_AddRefed<CreateElementTxn> +nsEditor::CreateTxnForCreateElement(nsIAtom& aTag, + nsINode& aParent, + int32_t aPosition) +{ + nsRefPtr<CreateElementTxn> txn = + new CreateElementTxn(*this, aTag, aParent, aPosition); + + return txn.forget(); +} + + +already_AddRefed<InsertNodeTxn> +nsEditor::CreateTxnForInsertNode(nsIContent& aNode, + nsINode& aParent, + int32_t aPosition) +{ + nsRefPtr<InsertNodeTxn> txn = new InsertNodeTxn(aNode, aParent, aPosition, + *this); + return txn.forget(); } nsresult nsEditor::CreateTxnForDeleteNode(nsINode* aNode, DeleteNodeTxn** aTxn) { NS_ENSURE_TRUE(aNode, NS_ERROR_NULL_POINTER); nsRefPtr<DeleteNodeTxn> txn = new DeleteNodeTxn(); @@ -4461,49 +4421,47 @@ nsEditor::CreateTxnForDeleteSelection(ED } } aggTxn.forget(aTxn); return NS_OK; } -nsresult -nsEditor::CreateTxnForDeleteCharacter(nsIDOMCharacterData* aData, - uint32_t aOffset, - EDirection aDirection, - DeleteTextTxn** aTxn) +already_AddRefed<DeleteTextTxn> +nsEditor::CreateTxnForDeleteCharacter(nsGenericDOMDataNode& aData, + uint32_t aOffset, EDirection aDirection) { NS_ASSERTION(aDirection == eNext || aDirection == ePrevious, - "invalid direction"); + "Invalid direction"); nsAutoString data; - aData->GetData(data); + aData.GetData(data); NS_ASSERTION(data.Length(), "Trying to delete from a zero-length node"); - NS_ENSURE_STATE(data.Length()); + NS_ENSURE_TRUE(data.Length(), nullptr); uint32_t segOffset = aOffset, segLength = 1; if (aDirection == eNext) { if (segOffset + 1 < data.Length() && NS_IS_HIGH_SURROGATE(data[segOffset]) && NS_IS_LOW_SURROGATE(data[segOffset+1])) { - // delete both halves of the surrogate pair + // Delete both halves of the surrogate pair ++segLength; } } else if (aOffset > 0) { --segOffset; if (segOffset > 0 && NS_IS_LOW_SURROGATE(data[segOffset]) && NS_IS_HIGH_SURROGATE(data[segOffset-1])) { ++segLength; --segOffset; } } else { - return NS_ERROR_FAILURE; - } - return CreateTxnForDeleteText(aData, segOffset, segLength, aTxn); + return nullptr; + } + return CreateTxnForDeleteText(aData, segOffset, segLength); } //XXX: currently, this doesn't handle edge conditions because GetNext/GetPrior //are not implemented nsresult nsEditor::CreateTxnForDeleteInsertionPoint(nsRange* aRange, EDirection aAction, EditAggregateTxn* aTxn, @@ -4518,17 +4476,16 @@ nsEditor::CreateTxnForDeleteInsertionPoi // get the node and offset of the insertion point nsCOMPtr<nsINode> node = aRange->GetStartParent(); NS_ENSURE_STATE(node); int32_t offset = aRange->StartOffset(); // determine if the insertion point is at the beginning, middle, or end of // the node - nsCOMPtr<nsIDOMCharacterData> nodeAsCharData = do_QueryInterface(node); uint32_t count = node->Length(); bool isFirst = (0 == offset); bool isLast = (count == (uint32_t)offset); // XXX: if isFirst && isLast, then we'll need to delete the node // as well as the 1 child @@ -4538,26 +4495,25 @@ nsEditor::CreateTxnForDeleteInsertionPoi if (aAction == ePrevious && isFirst) { // we're backspacing from the beginning of the node. Delete the first // thing to our left nsCOMPtr<nsIContent> priorNode = GetPriorNode(node, true); NS_ENSURE_STATE(priorNode); // there is a priorNode, so delete its last child (if chardata, delete the // last char). if it has no children, delete it - nsCOMPtr<nsIDOMCharacterData> priorNodeAsCharData = - do_QueryInterface(priorNode); - if (priorNodeAsCharData) { + if (priorNode->IsNodeOfType(nsINode::eDATA_NODE)) { + nsRefPtr<nsGenericDOMDataNode> priorNodeAsCharData = + static_cast<nsGenericDOMDataNode*>(priorNode.get()); uint32_t length = priorNode->Length(); // Bail out for empty chardata XXX: Do we want to do something else? NS_ENSURE_STATE(length); - nsRefPtr<DeleteTextTxn> txn; - res = CreateTxnForDeleteCharacter(priorNodeAsCharData, length, - ePrevious, getter_AddRefs(txn)); - NS_ENSURE_SUCCESS(res, res); + nsRefPtr<DeleteTextTxn> txn = + CreateTxnForDeleteCharacter(*priorNodeAsCharData, length, ePrevious); + NS_ENSURE_STATE(txn); *aOffset = txn->GetOffset(); *aLength = txn->GetNumCharsToDelete(); aTxn->AppendChild(txn); } else { // priorNode is not chardata, so tell its parent to delete it nsRefPtr<DeleteNodeTxn> txn; res = CreateTxnForDeleteNode(priorNode, getter_AddRefs(txn)); @@ -4574,26 +4530,25 @@ nsEditor::CreateTxnForDeleteInsertionPoi if (aAction == eNext && isLast) { // we're deleting from the end of the node. Delete the first thing to our // right nsCOMPtr<nsIContent> nextNode = GetNextNode(node, true); NS_ENSURE_STATE(nextNode); // there is a nextNode, so delete its first child (if chardata, delete the // first char). if it has no children, delete it - nsCOMPtr<nsIDOMCharacterData> nextNodeAsCharData = - do_QueryInterface(nextNode); - if (nextNodeAsCharData) { + if (nextNode->IsNodeOfType(nsINode::eDATA_NODE)) { + nsRefPtr<nsGenericDOMDataNode> nextNodeAsCharData = + static_cast<nsGenericDOMDataNode*>(nextNode.get()); uint32_t length = nextNode->Length(); // Bail out for empty chardata XXX: Do we want to do something else? NS_ENSURE_STATE(length); - nsRefPtr<DeleteTextTxn> txn; - res = CreateTxnForDeleteCharacter(nextNodeAsCharData, 0, eNext, - getter_AddRefs(txn)); - NS_ENSURE_SUCCESS(res, res); + nsRefPtr<DeleteTextTxn> txn = + CreateTxnForDeleteCharacter(*nextNodeAsCharData, 0, eNext); + NS_ENSURE_STATE(txn); *aOffset = txn->GetOffset(); *aLength = txn->GetNumCharsToDelete(); aTxn->AppendChild(txn); } else { // nextNode is not chardata, so tell its parent to delete it nsRefPtr<DeleteNodeTxn> txn; res = CreateTxnForDeleteNode(nextNode, getter_AddRefs(txn)); @@ -4601,22 +4556,23 @@ nsEditor::CreateTxnForDeleteInsertionPoi aTxn->AppendChild(txn); } NS_ADDREF(*aNode = nextNode); return NS_OK; } - if (nodeAsCharData) { + if (node->IsNodeOfType(nsINode::eDATA_NODE)) { + nsRefPtr<nsGenericDOMDataNode> nodeAsCharData = + static_cast<nsGenericDOMDataNode*>(node.get()); // we have chardata, so delete a char at the proper offset - nsRefPtr<DeleteTextTxn> txn; - res = CreateTxnForDeleteCharacter(nodeAsCharData, offset, aAction, - getter_AddRefs(txn)); - NS_ENSURE_SUCCESS(res, res); + nsRefPtr<DeleteTextTxn> txn = CreateTxnForDeleteCharacter(*nodeAsCharData, + offset, aAction); + NS_ENSURE_STATE(txn); aTxn->AppendChild(txn); NS_ADDREF(*aNode = node); *aOffset = txn->GetOffset(); *aLength = txn->GetNumCharsToDelete(); } else { // we're either deleting a node or chardata, need to dig into the next/prev // node to find out @@ -4634,28 +4590,27 @@ nsEditor::CreateTxnForDeleteInsertionPoi if (aAction == ePrevious) { selectedNode = GetPriorNode(selectedNode, true); } else if (aAction == eNext) { selectedNode = GetNextNode(selectedNode, true); } } NS_ENSURE_STATE(selectedNode); - nsCOMPtr<nsIDOMCharacterData> selectedNodeAsCharData = - do_QueryInterface(selectedNode); - if (selectedNodeAsCharData) { + if (selectedNode->IsNodeOfType(nsINode::eDATA_NODE)) { + nsRefPtr<nsGenericDOMDataNode> selectedNodeAsCharData = + static_cast<nsGenericDOMDataNode*>(selectedNode.get()); // we are deleting from a chardata node, so do a character deletion uint32_t position = 0; if (aAction == ePrevious) { position = selectedNode->Length(); } - nsRefPtr<DeleteTextTxn> delTextTxn; - res = CreateTxnForDeleteCharacter(selectedNodeAsCharData, position, - aAction, getter_AddRefs(delTextTxn)); - NS_ENSURE_SUCCESS(res, res); + nsRefPtr<DeleteTextTxn> delTextTxn = + CreateTxnForDeleteCharacter(*selectedNodeAsCharData, position, + aAction); NS_ENSURE_TRUE(delTextTxn, NS_ERROR_NULL_POINTER); aTxn->AppendChild(delTextTxn); *aOffset = delTextTxn->GetOffset(); *aLength = delTextTxn->GetNumCharsToDelete(); } else { nsRefPtr<DeleteNodeTxn> delElementTxn; res = CreateTxnForDeleteNode(selectedNode, getter_AddRefs(delElementTxn));
--- a/editor/libeditor/nsEditor.h +++ b/editor/libeditor/nsEditor.h @@ -24,22 +24,19 @@ #include "nsLiteralString.h" // for NS_LITERAL_STRING #include "nsSelectionState.h" // for nsRangeUpdater, etc #include "nsString.h" // for nsCString #include "nsWeakReference.h" // for nsSupportsWeakReference #include "nscore.h" // for nsresult, nsAString, etc class AddStyleSheetTxn; class ChangeAttributeTxn; -class CreateElementTxn; class DeleteNodeTxn; -class DeleteTextTxn; class EditAggregateTxn; class IMETextTxn; -class InsertElementTxn; class InsertTextTxn; class JoinElementTxn; class RemoveStyleSheetTxn; class SplitElementTxn; class nsIAtom; class nsIContent; class nsIDOMCharacterData; class nsIDOMDataTransfer; @@ -67,19 +64,22 @@ class nsString; class nsTransactionManager; namespace mozilla { class CSSStyleSheet; class ErrorResult; class TextComposition; namespace dom { +class CreateElementTxn; class DataTransfer; +class DeleteTextTxn; class Element; class EventTarget; +class InsertNodeTxn; class Selection; class Text; } // namespace dom } // namespace mozilla namespace mozilla { namespace widget { struct IMEState; @@ -195,17 +195,16 @@ public: /* ------------ nsIObserver methods -------------- */ NS_DECL_NSIOBSERVER // nsIPhonetic NS_DECL_NSIPHONETIC public: - nsresult MarkNodeDirty(nsINode* aNode); virtual bool IsModifiableNode(nsINode *aNode); NS_IMETHOD InsertTextImpl(const nsAString& aStringToInsert, nsCOMPtr<nsIDOMNode> *aInOutNode, int32_t *aInOutOffset, nsIDOMDocument *aDoc); nsresult InsertTextIntoTextNodeImpl(const nsAString& aStringToInsert, mozilla::dom::Text* aTextNode, @@ -217,43 +216,35 @@ public: bool aSuppressIME = false); NS_IMETHOD DeleteSelectionImpl(EDirection aAction, EStripWrappers aStripWrappers); NS_IMETHOD DeleteSelectionAndCreateNode(const nsAString& aTag, nsIDOMNode ** aNewNode); /* helper routines for node/parent manipulations */ nsresult DeleteNode(nsINode* aNode); - nsresult InsertNode(nsIContent* aContent, nsINode* aParent, - int32_t aPosition); + nsresult InsertNode(nsIContent& aNode, nsINode& aParent, int32_t aPosition); enum ECloneAttributes { eDontCloneAttributes, eCloneAttributes }; already_AddRefed<mozilla::dom::Element> ReplaceContainer( mozilla::dom::Element* aOldContainer, nsIAtom* aNodeType, nsIAtom* aAttribute = nullptr, const nsAString* aValue = nullptr, ECloneAttributes aCloneAttributes = eDontCloneAttributes); void CloneAttributes(mozilla::dom::Element* aDest, mozilla::dom::Element* aSource); - nsresult RemoveContainer(nsINode* aNode); - nsresult RemoveContainer(nsIDOMNode *inNode); - nsresult InsertContainerAbove(nsIContent* aNode, - mozilla::dom::Element** aOutNode, - const nsAString& aNodeType, - const nsAString* aAttribute = nullptr, + nsresult RemoveContainer(nsIContent* aNode); + already_AddRefed<mozilla::dom::Element> InsertContainerAbove( + nsIContent* aNode, + nsIAtom* aNodeType, + nsIAtom* aAttribute = nullptr, const nsAString* aValue = nullptr); - nsresult InsertContainerAbove(nsIDOMNode *inNode, - nsCOMPtr<nsIDOMNode> *outNode, - const nsAString &aNodeType, - const nsAString *aAttribute = nullptr, - const nsAString *aValue = nullptr); nsresult JoinNodes(nsINode* aNodeToKeep, nsIContent* aNodeToMove); - nsresult MoveNode(nsINode* aNode, nsINode* aParent, int32_t aOffset); - nsresult MoveNode(nsIDOMNode *aNode, nsIDOMNode *aParent, int32_t aOffset); + nsresult MoveNode(nsIContent* aNode, nsINode* aParent, int32_t aOffset); /* Method to replace certain CreateElementNS() calls. Arguments: nsIAtom* aTag - tag you want */ already_AddRefed<mozilla::dom::Element> CreateHTMLContent(nsIAtom* aTag); // IME event handlers @@ -277,27 +268,29 @@ protected: /** create a transaction for removing aAttribute on aElement */ NS_IMETHOD CreateTxnForRemoveAttribute(nsIDOMElement *aElement, const nsAString & aAttribute, ChangeAttributeTxn ** aTxn); /** create a transaction for creating a new child node of aParent of type aTag. */ - NS_IMETHOD CreateTxnForCreateElement(const nsAString & aTag, - nsIDOMNode *aParent, - int32_t aPosition, - CreateElementTxn ** aTxn); + already_AddRefed<mozilla::dom::CreateElementTxn> + CreateTxnForCreateElement(nsIAtom& aTag, + nsINode& aParent, + int32_t aPosition); + + already_AddRefed<mozilla::dom::Element> CreateNode(nsIAtom* aTag, + nsINode* aParent, + int32_t aPosition); /** create a transaction for inserting aNode as a child of aParent. */ - NS_IMETHOD CreateTxnForInsertElement(nsIDOMNode * aNode, - nsIDOMNode * aParent, - int32_t aOffset, - InsertElementTxn ** aTxn); + already_AddRefed<mozilla::dom::InsertNodeTxn> + CreateTxnForInsertNode(nsIContent& aNode, nsINode& aParent, int32_t aOffset); /** create a transaction for removing aNode from its parent. */ nsresult CreateTxnForDeleteNode(nsINode* aNode, DeleteNodeTxn** aTxn); nsresult CreateTxnForDeleteSelection(EDirection aAction, EditAggregateTxn** aTxn, @@ -329,38 +322,28 @@ protected: NS_IMETHOD CreateTxnForAddStyleSheet(mozilla::CSSStyleSheet* aSheet, AddStyleSheetTxn* *aTxn); /** create a transaction for removing a style sheet */ NS_IMETHOD CreateTxnForRemoveStyleSheet(mozilla::CSSStyleSheet* aSheet, RemoveStyleSheetTxn* *aTxn); - NS_IMETHOD DeleteText(nsIDOMCharacterData *aElement, - uint32_t aOffset, - uint32_t aLength); - - inline nsresult DeleteText(mozilla::dom::Text* aText, uint32_t aOffset, - uint32_t aLength) - { - return DeleteText(static_cast<nsIDOMCharacterData*>(GetAsDOMNode(aText)), - aOffset, aLength); - } + nsresult DeleteText(nsGenericDOMDataNode& aElement, + uint32_t aOffset, uint32_t aLength); // NS_IMETHOD DeleteRange(nsIDOMRange *aRange); - nsresult CreateTxnForDeleteText(nsIDOMCharacterData* aElement, - uint32_t aOffset, - uint32_t aLength, - DeleteTextTxn** aTxn); + already_AddRefed<mozilla::dom::DeleteTextTxn> + CreateTxnForDeleteText(nsGenericDOMDataNode& aElement, + uint32_t aOffset, uint32_t aLength); - nsresult CreateTxnForDeleteCharacter(nsIDOMCharacterData* aData, - uint32_t aOffset, - EDirection aDirection, - DeleteTextTxn** aTxn); + already_AddRefed<mozilla::dom::DeleteTextTxn> + CreateTxnForDeleteCharacter(nsGenericDOMDataNode& aData, uint32_t aOffset, + EDirection aDirection); NS_IMETHOD CreateTxnForSplitNode(nsIDOMNode *aNode, uint32_t aOffset, SplitElementTxn **aTxn); NS_IMETHOD CreateTxnForJoinNode(nsIDOMNode *aLeftNode, nsIDOMNode *aRightNode, JoinElementTxn **aTxn);
--- a/editor/libeditor/nsHTMLAbsPosition.cpp +++ b/editor/libeditor/nsHTMLAbsPosition.cpp @@ -564,17 +564,17 @@ nsHTMLEditor::AbsolutelyPositionElement( } nsCOMPtr<dom::Element> element = do_QueryInterface(aElement); if (element && element->IsHTML(nsGkAtoms::div) && !HasStyleOrIdOrClass(element)) { nsRefPtr<nsHTMLEditRules> htmlRules = static_cast<nsHTMLEditRules*>(mRules.get()); NS_ENSURE_TRUE(htmlRules, NS_ERROR_FAILURE); nsresult res = htmlRules->MakeSureElemStartsOrEndsOnCR(aElement); NS_ENSURE_SUCCESS(res, res); - res = RemoveContainer(aElement); + res = RemoveContainer(element); NS_ENSURE_SUCCESS(res, res); } } return NS_OK; } NS_IMETHODIMP nsHTMLEditor::SetSnapToGridEnabled(bool aEnabled)
--- a/editor/libeditor/nsHTMLCSSUtils.cpp +++ b/editor/libeditor/nsHTMLCSSUtils.cpp @@ -608,17 +608,17 @@ nsHTMLCSSUtils::RemoveCSSInlineStyle(nsI NS_ENSURE_SUCCESS(res, res); nsCOMPtr<dom::Element> element = do_QueryInterface(aNode); if (!element || !element->IsHTML(nsGkAtoms::span) || nsHTMLEditor::HasAttributes(element)) { return NS_OK; } - return mHTMLEditor->RemoveContainer(aNode); + return mHTMLEditor->RemoveContainer(element); } // Answers true is the property can be removed by setting a "none" CSS value // on a node bool nsHTMLCSSUtils::IsCSSInvertable(nsIAtom *aProperty, const nsAString *aAttribute) { return bool(nsEditProperty::b == aProperty);
--- a/editor/libeditor/nsHTMLEditRules.cpp +++ b/editor/libeditor/nsHTMLEditRules.cpp @@ -1748,17 +1748,17 @@ nsHTMLEditRules::StandardBreakImpl(nsIDO // from being in different inline nodes, which would break // SetInterlinePosition(). It will also assure that if the user clicks // away and then clicks back on their new blank line, they will still // get the style from the line above. int32_t brOffset; nsCOMPtr<nsIDOMNode> brParent = nsEditor::GetNodeLocation(GetAsDOMNode(secondBR), &brOffset); if (brParent != node || brOffset != aOffset + 1) { NS_ENSURE_STATE(mHTMLEditor); - res = mHTMLEditor->MoveNode(GetAsDOMNode(secondBR), node, aOffset+1); + res = mHTMLEditor->MoveNode(secondBR->AsContent(), node_, aOffset + 1); NS_ENSURE_SUCCESS(res, res); } } // SetInterlinePosition(true) means we want the caret to stick to the // content on the "right". We want the caret to stick to whatever is past // the break. This is because the break is on the same line we were on, // but the next content will be on the following line. @@ -2051,19 +2051,20 @@ nsHTMLEditRules::WillDeleteSelection(Sel res = range->GetEndOffset(&eo); NS_ENSURE_SUCCESS(res, res); } NS_ENSURE_STATE(mHTMLEditor); res = nsWSRunObject::PrepareToDeleteRange(mHTMLEditor, address_of(visNode_), &so, address_of(visNode_), &eo); NS_ENSURE_SUCCESS(res, res); visNode = GetAsDOMNode(visNode_); - nsCOMPtr<nsIDOMCharacterData> nodeAsText(do_QueryInterface(visNode)); - NS_ENSURE_STATE(mHTMLEditor); - res = mHTMLEditor->DeleteText(nodeAsText, std::min(so, eo), DeprecatedAbs(eo - so)); + nsRefPtr<Text> nodeAsText = visNode_->GetAsText(); + NS_ENSURE_STATE(mHTMLEditor); + res = mHTMLEditor->DeleteText(*nodeAsText, std::min(so, eo), + DeprecatedAbs(eo - so)); *aHandled = true; NS_ENSURE_SUCCESS(res, res); res = InsertBRIfNeeded(aSelection); return res; } else if (wsType == WSType::special || wsType == WSType::br || nsHTMLEditUtils::IsHR(visNode)) { // short circuit for invisible breaks. delete them and recurse. if (nsTextEditUtils::IsBreak(visNode) && @@ -2522,37 +2523,39 @@ nsHTMLEditRules::WillDeleteSelection(Sel // we can assume that if text node is found, we can // delete to end or to begining as appropriate, // since the case where both sel endpoints in same // text node was already handled (we wouldn't be here) NS_ENSURE_STATE(mHTMLEditor); if ( mHTMLEditor->IsTextNode(startNode) ) { // delete to last character - nsCOMPtr<nsIDOMCharacterData>nodeAsText; - uint32_t len; - nodeAsText = do_QueryInterface(startNode); - nodeAsText->GetLength(&len); + nsCOMPtr<nsINode> node = do_QueryInterface(startNode); + uint32_t len = node->Length(); if (len > (uint32_t)startOffset) { + nsRefPtr<nsGenericDOMDataNode> dataNode = + static_cast<nsGenericDOMDataNode*>(node.get()); NS_ENSURE_STATE(mHTMLEditor); - res = mHTMLEditor->DeleteText(nodeAsText,startOffset,len-startOffset); + res = mHTMLEditor->DeleteText(*dataNode, startOffset, + len - startOffset); NS_ENSURE_SUCCESS(res, res); } } NS_ENSURE_STATE(mHTMLEditor); if ( mHTMLEditor->IsTextNode(endNode) ) { // delete to first character - nsCOMPtr<nsIDOMCharacterData>nodeAsText; - nodeAsText = do_QueryInterface(endNode); + nsCOMPtr<nsINode> node = do_QueryInterface(endNode); if (endOffset) { NS_ENSURE_STATE(mHTMLEditor); - res = mHTMLEditor->DeleteText(nodeAsText,0,endOffset); + nsRefPtr<nsGenericDOMDataNode> dataNode = + static_cast<nsGenericDOMDataNode*>(node.get()); + res = mHTMLEditor->DeleteText(*dataNode, 0, endOffset); NS_ENSURE_SUCCESS(res, res); } } if (join) { res = JoinBlocks(leftParent, rightParent, aCancel); NS_ENSURE_SUCCESS(res, res); } @@ -2799,26 +2802,26 @@ nsHTMLEditRules::JoinBlocks(nsIDOMNode * // Do br adjustment. nsCOMPtr<nsIDOMNode> brNode; res = CheckForInvisibleBR(aLeftBlock, kBlockEnd, address_of(brNode)); NS_ENSURE_SUCCESS(res, res); if (bMergeLists) { // idea here is to take all children in rightList that are past // theOffset, and pull them into leftlist. - nsCOMPtr<nsIDOMNode> childToMove; nsCOMPtr<nsIContent> parent(do_QueryInterface(rightList)); NS_ENSURE_TRUE(parent, NS_ERROR_NULL_POINTER); nsIContent *child = parent->GetChildAt(theOffset); + nsCOMPtr<nsINode> leftList_ = do_QueryInterface(leftList); + NS_ENSURE_STATE(leftList_); while (child) { - childToMove = do_QueryInterface(child); NS_ENSURE_STATE(mHTMLEditor); - res = mHTMLEditor->MoveNode(childToMove, leftList, -1); + res = mHTMLEditor->MoveNode(child, leftList_, -1); NS_ENSURE_SUCCESS(res, res); child = parent->GetChildAt(rightOffset); } } else { res = MoveBlock(aLeftBlock, aRightBlock, leftOffset, rightOffset); @@ -3007,25 +3010,27 @@ nsHTMLEditRules::MoveBlock(nsIDOMNode *a * inserted content. * nsIDOMNode *aSource the selection. * nsIDOMNode *aDest parent to receive moved content * int32_t *aOffset offset in aDest to move content to */ nsresult nsHTMLEditRules::MoveNodeSmart(nsIDOMNode *aSource, nsIDOMNode *aDest, int32_t *aOffset) { - NS_ENSURE_TRUE(aSource && aDest && aOffset, NS_ERROR_NULL_POINTER); + nsCOMPtr<nsIContent> source = do_QueryInterface(aSource); + nsCOMPtr<nsINode> dest = do_QueryInterface(aDest); + NS_ENSURE_TRUE(source && dest && aOffset, NS_ERROR_NULL_POINTER); nsresult res; // check if this node can go into the destination node NS_ENSURE_STATE(mHTMLEditor); if (mHTMLEditor->CanContain(aDest, aSource)) { // if it can, move it there NS_ENSURE_STATE(mHTMLEditor); - res = mHTMLEditor->MoveNode(aSource, aDest, *aOffset); + res = mHTMLEditor->MoveNode(source, dest, *aOffset); NS_ENSURE_SUCCESS(res, res); if (*aOffset != -1) ++(*aOffset); } else