michael@0: #filter substitution michael@0: // -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: "use strict"; michael@0: michael@0: let Cc = Components.classes; michael@0: let Ci = Components.interfaces; michael@0: let Cu = Components.utils; michael@0: let Cr = Components.results; michael@0: michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/AddonManager.jsm"); michael@0: Cu.import("resource://gre/modules/FileUtils.jsm"); michael@0: Cu.import("resource://gre/modules/JNI.jsm"); michael@0: Cu.import('resource://gre/modules/Payment.jsm'); michael@0: Cu.import("resource://gre/modules/NotificationDB.jsm"); michael@0: Cu.import("resource://gre/modules/SpatialNavigation.jsm"); michael@0: Cu.import("resource://gre/modules/UITelemetry.jsm"); michael@0: michael@0: #ifdef ACCESSIBILITY michael@0: Cu.import("resource://gre/modules/accessibility/AccessFu.jsm"); michael@0: #endif michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PluralForm", michael@0: "resource://gre/modules/PluralForm.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "sendMessageToJava", michael@0: "resource://gre/modules/Messaging.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "DebuggerServer", michael@0: "resource://gre/modules/devtools/dbg-server.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "UserAgentOverrides", michael@0: "resource://gre/modules/UserAgentOverrides.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "LoginManagerContent", michael@0: "resource://gre/modules/LoginManagerContent.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "OS", "resource://gre/modules/osfile.jsm"); michael@0: michael@0: #ifdef MOZ_SAFE_BROWSING michael@0: XPCOMUtils.defineLazyModuleGetter(this, "SafeBrowsing", michael@0: "resource://gre/modules/SafeBrowsing.jsm"); michael@0: #endif michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", michael@0: "resource://gre/modules/PrivateBrowsingUtils.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Sanitizer", michael@0: "resource://gre/modules/Sanitizer.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Prompt", michael@0: "resource://gre/modules/Prompt.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "HelperApps", michael@0: "resource://gre/modules/HelperApps.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "SSLExceptions", michael@0: "resource://gre/modules/SSLExceptions.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "FormHistory", michael@0: "resource://gre/modules/FormHistory.jsm"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "uuidgen", michael@0: "@mozilla.org/uuid-generator;1", michael@0: "nsIUUIDGenerator"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "SimpleServiceDiscovery", michael@0: "resource://gre/modules/SimpleServiceDiscovery.jsm"); michael@0: michael@0: #ifdef NIGHTLY_BUILD michael@0: XPCOMUtils.defineLazyModuleGetter(this, "ShumwayUtils", michael@0: "resource://shumway/ShumwayUtils.jsm"); michael@0: #endif michael@0: michael@0: #ifdef MOZ_ANDROID_SYNTHAPKS michael@0: XPCOMUtils.defineLazyModuleGetter(this, "WebappManager", michael@0: "resource://gre/modules/WebappManager.jsm"); michael@0: #endif michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu", michael@0: "resource://gre/modules/CharsetMenu.jsm"); michael@0: michael@0: // Lazily-loaded browser scripts: michael@0: [ michael@0: ["SelectHelper", "chrome://browser/content/SelectHelper.js"], michael@0: ["InputWidgetHelper", "chrome://browser/content/InputWidgetHelper.js"], michael@0: ["AboutReader", "chrome://browser/content/aboutReader.js"], michael@0: ["MasterPassword", "chrome://browser/content/MasterPassword.js"], michael@0: ["PluginHelper", "chrome://browser/content/PluginHelper.js"], michael@0: ["OfflineApps", "chrome://browser/content/OfflineApps.js"], michael@0: ["Linkifier", "chrome://browser/content/Linkify.js"], michael@0: ["ZoomHelper", "chrome://browser/content/ZoomHelper.js"], michael@0: ["CastingApps", "chrome://browser/content/CastingApps.js"], michael@0: ].forEach(function (aScript) { michael@0: let [name, script] = aScript; michael@0: XPCOMUtils.defineLazyGetter(window, name, function() { michael@0: let sandbox = {}; michael@0: Services.scriptloader.loadSubScript(script, sandbox); michael@0: return sandbox[name]; michael@0: }); michael@0: }); michael@0: michael@0: [ michael@0: #ifdef MOZ_WEBRTC michael@0: ["WebrtcUI", ["getUserMedia:request", "recording-device-events"], "chrome://browser/content/WebrtcUI.js"], michael@0: #endif michael@0: ["MemoryObserver", ["memory-pressure", "Memory:Dump"], "chrome://browser/content/MemoryObserver.js"], michael@0: ["ConsoleAPI", ["console-api-log-event"], "chrome://browser/content/ConsoleAPI.js"], michael@0: ["FindHelper", ["FindInPage:Find", "FindInPage:Prev", "FindInPage:Next", "FindInPage:Closed", "Tab:Selected"], "chrome://browser/content/FindHelper.js"], michael@0: ["PermissionsHelper", ["Permissions:Get", "Permissions:Clear"], "chrome://browser/content/PermissionsHelper.js"], michael@0: ["FeedHandler", ["Feeds:Subscribe"], "chrome://browser/content/FeedHandler.js"], michael@0: ["Feedback", ["Feedback:Show"], "chrome://browser/content/Feedback.js"], michael@0: ["SelectionHandler", ["TextSelection:Get"], "chrome://browser/content/SelectionHandler.js"], michael@0: ].forEach(function (aScript) { michael@0: let [name, notifications, script] = aScript; michael@0: XPCOMUtils.defineLazyGetter(window, name, function() { michael@0: let sandbox = {}; michael@0: Services.scriptloader.loadSubScript(script, sandbox); michael@0: return sandbox[name]; michael@0: }); michael@0: notifications.forEach(function (aNotification) { michael@0: Services.obs.addObserver(function(s, t, d) { michael@0: window[name].observe(s, t, d) michael@0: }, aNotification, false); michael@0: }); michael@0: }); michael@0: michael@0: // Lazily-loaded JS modules that use observer notifications michael@0: [ michael@0: ["Home", ["HomeBanner:Get", "HomePanels:Get", "HomePanels:Authenticate", "HomePanels:RefreshView", michael@0: "HomePanels:Installed", "HomePanels:Uninstalled"], "resource://gre/modules/Home.jsm"], michael@0: ].forEach(module => { michael@0: let [name, notifications, resource] = module; michael@0: XPCOMUtils.defineLazyModuleGetter(this, name, resource); michael@0: notifications.forEach(notification => { michael@0: Services.obs.addObserver((s,t,d) => { michael@0: this[name].observe(s,t,d) michael@0: }, notification, false); michael@0: }); michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "Haptic", michael@0: "@mozilla.org/widget/hapticfeedback;1", "nsIHapticFeedback"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "DOMUtils", michael@0: "@mozilla.org/inspector/dom-utils;1", "inIDOMUtils"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(window, "URIFixup", michael@0: "@mozilla.org/docshell/urifixup;1", "nsIURIFixup"); michael@0: michael@0: #ifdef MOZ_WEBRTC michael@0: XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService", michael@0: "@mozilla.org/mediaManagerService;1", "nsIMediaManagerService"); michael@0: #endif michael@0: michael@0: const kStateActive = 0x00000001; // :active pseudoclass for elements michael@0: michael@0: const kXLinkNamespace = "http://www.w3.org/1999/xlink"; michael@0: michael@0: const kDefaultCSSViewportWidth = 980; michael@0: const kDefaultCSSViewportHeight = 480; michael@0: michael@0: const kViewportRemeasureThrottle = 500; michael@0: michael@0: const kDoNotTrackPrefState = Object.freeze({ michael@0: NO_PREF: "0", michael@0: DISALLOW_TRACKING: "1", michael@0: ALLOW_TRACKING: "2", michael@0: }); michael@0: michael@0: function dump(a) { michael@0: Services.console.logStringMessage(a); michael@0: } michael@0: michael@0: function doChangeMaxLineBoxWidth(aWidth) { michael@0: gReflowPending = null; michael@0: let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation); michael@0: let docShell = webNav.QueryInterface(Ci.nsIDocShell); michael@0: let docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer); michael@0: michael@0: let range = null; michael@0: if (BrowserApp.selectedTab._mReflozPoint) { michael@0: range = BrowserApp.selectedTab._mReflozPoint.range; michael@0: } michael@0: michael@0: try { michael@0: docViewer.pausePainting(); michael@0: docViewer.changeMaxLineBoxWidth(aWidth); michael@0: michael@0: if (range) { michael@0: ZoomHelper.zoomInAndSnapToRange(range); michael@0: } else { michael@0: // In this case, we actually didn't zoom into a specific range. It michael@0: // probably happened from a page load reflow-on-zoom event, so we michael@0: // need to make sure painting is re-enabled. michael@0: BrowserApp.selectedTab.clearReflowOnZoomPendingActions(); michael@0: } michael@0: } finally { michael@0: docViewer.resumePainting(); michael@0: } michael@0: } michael@0: michael@0: function fuzzyEquals(a, b) { michael@0: return (Math.abs(a - b) < 1e-6); michael@0: } michael@0: michael@0: /** michael@0: * Convert a font size to CSS pixels (px) from twentieiths-of-a-point michael@0: * (twips). michael@0: */ michael@0: function convertFromTwipsToPx(aSize) { michael@0: return aSize/240 * 16.0; michael@0: } michael@0: michael@0: #ifdef MOZ_CRASHREPORTER michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter", michael@0: "@mozilla.org/xre/app-info;1", "nsICrashReporter"); michael@0: #endif michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "ContentAreaUtils", function() { michael@0: let ContentAreaUtils = {}; michael@0: Services.scriptloader.loadSubScript("chrome://global/content/contentAreaUtils.js", ContentAreaUtils); michael@0: return ContentAreaUtils; michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Rect", michael@0: "resource://gre/modules/Geometry.jsm"); michael@0: michael@0: function resolveGeckoURI(aURI) { michael@0: if (!aURI) michael@0: throw "Can't resolve an empty uri"; michael@0: michael@0: if (aURI.startsWith("chrome://")) { michael@0: let registry = Cc['@mozilla.org/chrome/chrome-registry;1'].getService(Ci["nsIChromeRegistry"]); michael@0: return registry.convertChromeURL(Services.io.newURI(aURI, null, null)).spec; michael@0: } else if (aURI.startsWith("resource://")) { michael@0: let handler = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler); michael@0: return handler.resolveURI(Services.io.newURI(aURI, null, null)); michael@0: } michael@0: return aURI; michael@0: } michael@0: michael@0: /** michael@0: * Cache of commonly used string bundles. michael@0: */ michael@0: var Strings = {}; michael@0: [ michael@0: ["brand", "chrome://branding/locale/brand.properties"], michael@0: ["browser", "chrome://browser/locale/browser.properties"] michael@0: ].forEach(function (aStringBundle) { michael@0: let [name, bundle] = aStringBundle; michael@0: XPCOMUtils.defineLazyGetter(Strings, name, function() { michael@0: return Services.strings.createBundle(bundle); michael@0: }); michael@0: }); michael@0: michael@0: const kFormHelperModeDisabled = 0; michael@0: const kFormHelperModeEnabled = 1; michael@0: const kFormHelperModeDynamic = 2; // disabled on tablets michael@0: michael@0: var BrowserApp = { michael@0: _tabs: [], michael@0: _selectedTab: null, michael@0: _prefObservers: [], michael@0: isGuest: false, michael@0: michael@0: get isTablet() { michael@0: let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2); michael@0: delete this.isTablet; michael@0: return this.isTablet = sysInfo.get("tablet"); michael@0: }, michael@0: michael@0: get isOnLowMemoryPlatform() { michael@0: let memory = Cc["@mozilla.org/xpcom/memory-service;1"].getService(Ci.nsIMemory); michael@0: delete this.isOnLowMemoryPlatform; michael@0: return this.isOnLowMemoryPlatform = memory.isLowMemoryPlatform(); michael@0: }, michael@0: michael@0: deck: null, michael@0: michael@0: startup: function startup() { michael@0: window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = new nsBrowserAccess(); michael@0: dump("zerdatime " + Date.now() + " - browser chrome startup finished."); michael@0: michael@0: this.deck = document.getElementById("browsers"); michael@0: this.deck.addEventListener("DOMContentLoaded", function BrowserApp_delayedStartup() { michael@0: try { michael@0: BrowserApp.deck.removeEventListener("DOMContentLoaded", BrowserApp_delayedStartup, false); michael@0: Services.obs.notifyObservers(window, "browser-delayed-startup-finished", ""); michael@0: sendMessageToJava({ type: "Gecko:DelayedStartup" }); michael@0: } catch(ex) { console.log(ex); } michael@0: }, false); michael@0: michael@0: BrowserEventHandler.init(); michael@0: ViewportHandler.init(); michael@0: michael@0: Services.androidBridge.browserApp = this; michael@0: michael@0: Services.obs.addObserver(this, "Locale:Changed", false); michael@0: Services.obs.addObserver(this, "Tab:Load", false); michael@0: Services.obs.addObserver(this, "Tab:Selected", false); michael@0: Services.obs.addObserver(this, "Tab:Closed", false); michael@0: Services.obs.addObserver(this, "Session:Back", false); michael@0: Services.obs.addObserver(this, "Session:ShowHistory", false); michael@0: Services.obs.addObserver(this, "Session:Forward", false); michael@0: Services.obs.addObserver(this, "Session:Reload", false); michael@0: Services.obs.addObserver(this, "Session:Stop", false); michael@0: Services.obs.addObserver(this, "SaveAs:PDF", false); michael@0: Services.obs.addObserver(this, "Browser:Quit", false); michael@0: Services.obs.addObserver(this, "Preferences:Set", false); michael@0: Services.obs.addObserver(this, "ScrollTo:FocusedInput", false); michael@0: Services.obs.addObserver(this, "Sanitize:ClearData", false); michael@0: Services.obs.addObserver(this, "FullScreen:Exit", false); michael@0: Services.obs.addObserver(this, "Viewport:Change", false); michael@0: Services.obs.addObserver(this, "Viewport:Flush", false); michael@0: Services.obs.addObserver(this, "Viewport:FixedMarginsChanged", false); michael@0: Services.obs.addObserver(this, "Passwords:Init", false); michael@0: Services.obs.addObserver(this, "FormHistory:Init", false); michael@0: Services.obs.addObserver(this, "gather-telemetry", false); michael@0: Services.obs.addObserver(this, "keyword-search", false); michael@0: #ifdef MOZ_ANDROID_SYNTHAPKS michael@0: Services.obs.addObserver(this, "webapps-runtime-install", false); michael@0: Services.obs.addObserver(this, "webapps-runtime-install-package", false); michael@0: Services.obs.addObserver(this, "webapps-ask-install", false); michael@0: Services.obs.addObserver(this, "webapps-launch", false); michael@0: Services.obs.addObserver(this, "webapps-uninstall", false); michael@0: Services.obs.addObserver(this, "Webapps:AutoInstall", false); michael@0: Services.obs.addObserver(this, "Webapps:Load", false); michael@0: Services.obs.addObserver(this, "Webapps:AutoUninstall", false); michael@0: #endif michael@0: Services.obs.addObserver(this, "sessionstore-state-purge-complete", false); michael@0: michael@0: function showFullScreenWarning() { michael@0: NativeWindow.toast.show(Strings.browser.GetStringFromName("alertFullScreenToast"), "short"); michael@0: } michael@0: michael@0: window.addEventListener("fullscreen", function() { michael@0: sendMessageToJava({ michael@0: type: window.fullScreen ? "ToggleChrome:Show" : "ToggleChrome:Hide" michael@0: }); michael@0: }, false); michael@0: michael@0: window.addEventListener("mozfullscreenchange", function() { michael@0: sendMessageToJava({ michael@0: type: document.mozFullScreen ? "DOMFullScreen:Start" : "DOMFullScreen:Stop" michael@0: }); michael@0: michael@0: if (document.mozFullScreen) michael@0: showFullScreenWarning(); michael@0: }, false); michael@0: michael@0: // When a restricted key is pressed in DOM full-screen mode, we should display michael@0: // the "Press ESC to exit" warning message. michael@0: window.addEventListener("MozShowFullScreenWarning", showFullScreenWarning, true); michael@0: michael@0: NativeWindow.init(); michael@0: LightWeightThemeWebInstaller.init(); michael@0: Downloads.init(); michael@0: FormAssistant.init(); michael@0: IndexedDB.init(); michael@0: HealthReportStatusListener.init(); michael@0: XPInstallObserver.init(); michael@0: CharacterEncoding.init(); michael@0: ActivityObserver.init(); michael@0: #ifdef MOZ_ANDROID_SYNTHAPKS michael@0: // TODO: replace with Android implementation of WebappOSUtils.isLaunchable. michael@0: Cu.import("resource://gre/modules/Webapps.jsm"); michael@0: DOMApplicationRegistry.allAppsLaunchable = true; michael@0: #else michael@0: WebappsUI.init(); michael@0: #endif michael@0: RemoteDebugger.init(); michael@0: Reader.init(); michael@0: UserAgentOverrides.init(); michael@0: DesktopUserAgent.init(); michael@0: CastingApps.init(); michael@0: Distribution.init(); michael@0: Tabs.init(); michael@0: #ifdef ACCESSIBILITY michael@0: AccessFu.attach(window); michael@0: #endif michael@0: #ifdef NIGHTLY_BUILD michael@0: ShumwayUtils.init(); michael@0: #endif michael@0: michael@0: // Init LoginManager michael@0: Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager); michael@0: michael@0: let url = null; michael@0: let pinned = false; michael@0: if ("arguments" in window) { michael@0: if (window.arguments[0]) michael@0: url = window.arguments[0]; michael@0: if (window.arguments[1]) michael@0: gScreenWidth = window.arguments[1]; michael@0: if (window.arguments[2]) michael@0: gScreenHeight = window.arguments[2]; michael@0: if (window.arguments[3]) michael@0: pinned = window.arguments[3]; michael@0: if (window.arguments[4]) michael@0: this.isGuest = window.arguments[4]; michael@0: } michael@0: michael@0: if (pinned) { michael@0: this._initRuntime(this._startupStatus, url, aUrl => this.addTab(aUrl)); michael@0: } else { michael@0: SearchEngines.init(); michael@0: this.initContextMenu(); michael@0: } michael@0: // The order that context menu items are added is important michael@0: // Make sure the "Open in App" context menu item appears at the bottom of the list michael@0: ExternalApps.init(); michael@0: michael@0: // XXX maybe we don't do this if the launch was kicked off from external michael@0: Services.io.offline = false; michael@0: michael@0: // Broadcast a UIReady message so add-ons know we are finished with startup michael@0: let event = document.createEvent("Events"); michael@0: event.initEvent("UIReady", true, false); michael@0: window.dispatchEvent(event); michael@0: michael@0: if (this._startupStatus) michael@0: this.onAppUpdated(); michael@0: michael@0: // Store the low-precision buffer pref michael@0: this.gUseLowPrecision = Services.prefs.getBoolPref("layers.low-precision-buffer"); michael@0: michael@0: // notify java that gecko has loaded michael@0: sendMessageToJava({ type: "Gecko:Ready" }); michael@0: michael@0: #ifdef MOZ_SAFE_BROWSING michael@0: // Bug 778855 - Perf regression if we do this here. To be addressed in bug 779008. michael@0: setTimeout(function() { SafeBrowsing.init(); }, 5000); michael@0: #endif michael@0: }, michael@0: michael@0: get _startupStatus() { michael@0: delete this._startupStatus; michael@0: michael@0: let savedMilestone = null; michael@0: try { michael@0: savedMilestone = Services.prefs.getCharPref("browser.startup.homepage_override.mstone"); michael@0: } catch (e) { michael@0: } michael@0: #expand let ourMilestone = "__MOZ_APP_VERSION__"; michael@0: this._startupStatus = ""; michael@0: if (ourMilestone != savedMilestone) { michael@0: Services.prefs.setCharPref("browser.startup.homepage_override.mstone", ourMilestone); michael@0: this._startupStatus = savedMilestone ? "upgrade" : "new"; michael@0: } michael@0: michael@0: return this._startupStatus; michael@0: }, michael@0: michael@0: /** michael@0: * Pass this a locale string, such as "fr" or "es_ES". michael@0: */ michael@0: setLocale: function (locale) { michael@0: console.log("browser.js: requesting locale set: " + locale); michael@0: sendMessageToJava({ type: "Locale:Set", locale: locale }); michael@0: }, michael@0: michael@0: _initRuntime: function(status, url, callback) { michael@0: let sandbox = {}; michael@0: Services.scriptloader.loadSubScript("chrome://browser/content/WebappRT.js", sandbox); michael@0: window.WebappRT = sandbox.WebappRT; michael@0: WebappRT.init(status, url, callback); michael@0: }, michael@0: michael@0: initContextMenu: function ba_initContextMenu() { michael@0: // TODO: These should eventually move into more appropriate classes michael@0: NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.openInNewTab"), michael@0: NativeWindow.contextmenus.linkOpenableNonPrivateContext, michael@0: function(aTarget) { michael@0: UITelemetry.addEvent("action.1", "contextmenu", null, "web_open_new_tab"); michael@0: UITelemetry.addEvent("loadurl.1", "contextmenu", null); michael@0: michael@0: let url = NativeWindow.contextmenus._getLinkURL(aTarget); michael@0: ContentAreaUtils.urlSecurityCheck(url, aTarget.ownerDocument.nodePrincipal); michael@0: BrowserApp.addTab(url, { selected: false, parentId: BrowserApp.selectedTab.id }); michael@0: michael@0: let newtabStrings = Strings.browser.GetStringFromName("newtabpopup.opened"); michael@0: let label = PluralForm.get(1, newtabStrings).replace("#1", 1); michael@0: NativeWindow.toast.show(label, "short"); michael@0: }); michael@0: michael@0: NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.openInPrivateTab"), michael@0: NativeWindow.contextmenus.linkOpenableContext, michael@0: function(aTarget) { michael@0: UITelemetry.addEvent("action.1", "contextmenu", null, "web_open_private_tab"); michael@0: UITelemetry.addEvent("loadurl.1", "contextmenu", null); michael@0: michael@0: let url = NativeWindow.contextmenus._getLinkURL(aTarget); michael@0: ContentAreaUtils.urlSecurityCheck(url, aTarget.ownerDocument.nodePrincipal); michael@0: BrowserApp.addTab(url, { selected: false, parentId: BrowserApp.selectedTab.id, isPrivate: true }); michael@0: michael@0: let newtabStrings = Strings.browser.GetStringFromName("newprivatetabpopup.opened"); michael@0: let label = PluralForm.get(1, newtabStrings).replace("#1", 1); michael@0: NativeWindow.toast.show(label, "short"); michael@0: }); michael@0: michael@0: NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyLink"), michael@0: NativeWindow.contextmenus.linkCopyableContext, michael@0: function(aTarget) { michael@0: UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_link"); michael@0: michael@0: let url = NativeWindow.contextmenus._getLinkURL(aTarget); michael@0: NativeWindow.contextmenus._copyStringToDefaultClipboard(url); michael@0: }); michael@0: michael@0: NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyEmailAddress"), michael@0: NativeWindow.contextmenus.emailLinkContext, michael@0: function(aTarget) { michael@0: UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_email"); michael@0: michael@0: let url = NativeWindow.contextmenus._getLinkURL(aTarget); michael@0: let emailAddr = NativeWindow.contextmenus._stripScheme(url); michael@0: NativeWindow.contextmenus._copyStringToDefaultClipboard(emailAddr); michael@0: }); michael@0: michael@0: NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyPhoneNumber"), michael@0: NativeWindow.contextmenus.phoneNumberLinkContext, michael@0: function(aTarget) { michael@0: UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_phone"); michael@0: michael@0: let url = NativeWindow.contextmenus._getLinkURL(aTarget); michael@0: let phoneNumber = NativeWindow.contextmenus._stripScheme(url); michael@0: NativeWindow.contextmenus._copyStringToDefaultClipboard(phoneNumber); michael@0: }); michael@0: michael@0: NativeWindow.contextmenus.add({ michael@0: label: Strings.browser.GetStringFromName("contextmenu.shareLink"), michael@0: order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, // Show above HTML5 menu items michael@0: selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.linkShareableContext), michael@0: showAsActions: function(aElement) { michael@0: return { michael@0: title: aElement.textContent.trim() || aElement.title.trim(), michael@0: uri: NativeWindow.contextmenus._getLinkURL(aElement), michael@0: }; michael@0: }, michael@0: icon: "drawable://ic_menu_share", michael@0: callback: function(aTarget) { michael@0: UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_link"); michael@0: } michael@0: }); michael@0: michael@0: NativeWindow.contextmenus.add({ michael@0: label: Strings.browser.GetStringFromName("contextmenu.shareEmailAddress"), michael@0: order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, michael@0: selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.emailLinkContext), michael@0: showAsActions: function(aElement) { michael@0: let url = NativeWindow.contextmenus._getLinkURL(aElement); michael@0: let emailAddr = NativeWindow.contextmenus._stripScheme(url); michael@0: let title = aElement.textContent || aElement.title; michael@0: return { michael@0: title: title, michael@0: uri: emailAddr, michael@0: }; michael@0: }, michael@0: icon: "drawable://ic_menu_share", michael@0: callback: function(aTarget) { michael@0: UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_email"); michael@0: } michael@0: }); michael@0: michael@0: NativeWindow.contextmenus.add({ michael@0: label: Strings.browser.GetStringFromName("contextmenu.sharePhoneNumber"), michael@0: order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, michael@0: selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.phoneNumberLinkContext), michael@0: showAsActions: function(aElement) { michael@0: let url = NativeWindow.contextmenus._getLinkURL(aElement); michael@0: let phoneNumber = NativeWindow.contextmenus._stripScheme(url); michael@0: let title = aElement.textContent || aElement.title; michael@0: return { michael@0: title: title, michael@0: uri: phoneNumber, michael@0: }; michael@0: }, michael@0: icon: "drawable://ic_menu_share", michael@0: callback: function(aTarget) { michael@0: UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_phone"); michael@0: } michael@0: }); michael@0: michael@0: NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.addToContacts"), michael@0: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.emailLinkContext), michael@0: function(aTarget) { michael@0: UITelemetry.addEvent("action.1", "contextmenu", null, "web_contact_email"); michael@0: michael@0: let url = NativeWindow.contextmenus._getLinkURL(aTarget); michael@0: sendMessageToJava({ michael@0: type: "Contact:Add", michael@0: email: url michael@0: }); michael@0: }); michael@0: michael@0: NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.addToContacts"), michael@0: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.phoneNumberLinkContext), michael@0: function(aTarget) { michael@0: UITelemetry.addEvent("action.1", "contextmenu", null, "web_contact_phone"); michael@0: michael@0: let url = NativeWindow.contextmenus._getLinkURL(aTarget); michael@0: sendMessageToJava({ michael@0: type: "Contact:Add", michael@0: phone: url michael@0: }); michael@0: }); michael@0: michael@0: NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.bookmarkLink"), michael@0: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.linkBookmarkableContext), michael@0: function(aTarget) { michael@0: UITelemetry.addEvent("action.1", "contextmenu", null, "web_bookmark"); michael@0: michael@0: let url = NativeWindow.contextmenus._getLinkURL(aTarget); michael@0: let title = aTarget.textContent || aTarget.title || url; michael@0: sendMessageToJava({ michael@0: type: "Bookmark:Insert", michael@0: url: url, michael@0: title: title michael@0: }); michael@0: }); michael@0: michael@0: NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.playMedia"), michael@0: NativeWindow.contextmenus.mediaContext("media-paused"), michael@0: function(aTarget) { michael@0: UITelemetry.addEvent("action.1", "contextmenu", null, "web_play"); michael@0: aTarget.play(); michael@0: }); michael@0: michael@0: NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.pauseMedia"), michael@0: NativeWindow.contextmenus.mediaContext("media-playing"), michael@0: function(aTarget) { michael@0: UITelemetry.addEvent("action.1", "contextmenu", null, "web_pause"); michael@0: aTarget.pause(); michael@0: }); michael@0: michael@0: NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.showControls2"), michael@0: NativeWindow.contextmenus.mediaContext("media-hidingcontrols"), michael@0: function(aTarget) { michael@0: UITelemetry.addEvent("action.1", "contextmenu", null, "web_controls_media"); michael@0: aTarget.setAttribute("controls", true); michael@0: }); michael@0: michael@0: NativeWindow.contextmenus.add({ michael@0: label: Strings.browser.GetStringFromName("contextmenu.shareMedia"), michael@0: order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, michael@0: selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.SelectorContext("video")), michael@0: showAsActions: function(aElement) { michael@0: let url = (aElement.currentSrc || aElement.src); michael@0: let title = aElement.textContent || aElement.title; michael@0: return { michael@0: title: title, michael@0: uri: url, michael@0: type: "video/*", michael@0: }; michael@0: }, michael@0: icon: "drawable://ic_menu_share", michael@0: callback: function(aTarget) { michael@0: UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_media"); michael@0: } michael@0: }); michael@0: michael@0: NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.fullScreen"), michael@0: NativeWindow.contextmenus.SelectorContext("video:not(:-moz-full-screen)"), michael@0: function(aTarget) { michael@0: UITelemetry.addEvent("action.1", "contextmenu", null, "web_fullscreen"); michael@0: aTarget.mozRequestFullScreen(); michael@0: }); michael@0: michael@0: NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.mute"), michael@0: NativeWindow.contextmenus.mediaContext("media-unmuted"), michael@0: function(aTarget) { michael@0: UITelemetry.addEvent("action.1", "contextmenu", null, "web_mute"); michael@0: aTarget.muted = true; michael@0: }); michael@0: michael@0: NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.unmute"), michael@0: NativeWindow.contextmenus.mediaContext("media-muted"), michael@0: function(aTarget) { michael@0: UITelemetry.addEvent("action.1", "contextmenu", null, "web_unmute"); michael@0: aTarget.muted = false; michael@0: }); michael@0: michael@0: NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.copyImageLocation"), michael@0: NativeWindow.contextmenus.imageLocationCopyableContext, michael@0: function(aTarget) { michael@0: UITelemetry.addEvent("action.1", "contextmenu", null, "web_copy_image"); michael@0: michael@0: let url = aTarget.src; michael@0: NativeWindow.contextmenus._copyStringToDefaultClipboard(url); michael@0: }); michael@0: michael@0: NativeWindow.contextmenus.add({ michael@0: label: Strings.browser.GetStringFromName("contextmenu.shareImage"), michael@0: selector: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.imageSaveableContext), michael@0: order: NativeWindow.contextmenus.DEFAULT_HTML5_ORDER - 1, // Show above HTML5 menu items michael@0: showAsActions: function(aTarget) { michael@0: let doc = aTarget.ownerDocument; michael@0: let imageCache = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools) michael@0: .getImgCacheForDocument(doc); michael@0: let props = imageCache.findEntryProperties(aTarget.currentURI, doc.characterSet); michael@0: let src = aTarget.src; michael@0: return { michael@0: title: src, michael@0: uri: src, michael@0: type: "image/*", michael@0: }; michael@0: }, michael@0: icon: "drawable://ic_menu_share", michael@0: menu: true, michael@0: callback: function(aTarget) { michael@0: UITelemetry.addEvent("action.1", "contextmenu", null, "web_share_image"); michael@0: } michael@0: }); michael@0: michael@0: NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.saveImage"), michael@0: NativeWindow.contextmenus.imageSaveableContext, michael@0: function(aTarget) { michael@0: UITelemetry.addEvent("action.1", "contextmenu", null, "web_save_image"); michael@0: michael@0: ContentAreaUtils.saveImageURL(aTarget.currentURI.spec, null, "SaveImageTitle", michael@0: false, true, aTarget.ownerDocument.documentURIObject, michael@0: aTarget.ownerDocument); michael@0: }); michael@0: michael@0: NativeWindow.contextmenus.add(Strings.browser.GetStringFromName("contextmenu.setImageAs"), michael@0: NativeWindow.contextmenus._disableInGuest(NativeWindow.contextmenus.imageSaveableContext), michael@0: function(aTarget) { michael@0: UITelemetry.addEvent("action.1", "contextmenu", null, "web_background_image"); michael@0: michael@0: let src = aTarget.src; michael@0: sendMessageToJava({ michael@0: type: "Image:SetAs", michael@0: url: src michael@0: }); michael@0: }); michael@0: michael@0: NativeWindow.contextmenus.add( michael@0: function(aTarget) { michael@0: if (aTarget instanceof HTMLVideoElement) { michael@0: // If a video element is zero width or height, its essentially michael@0: // an HTMLAudioElement. michael@0: if (aTarget.videoWidth == 0 || aTarget.videoHeight == 0 ) michael@0: return Strings.browser.GetStringFromName("contextmenu.saveAudio"); michael@0: return Strings.browser.GetStringFromName("contextmenu.saveVideo"); michael@0: } else if (aTarget instanceof HTMLAudioElement) { michael@0: return Strings.browser.GetStringFromName("contextmenu.saveAudio"); michael@0: } michael@0: return Strings.browser.GetStringFromName("contextmenu.saveVideo"); michael@0: }, NativeWindow.contextmenus.mediaSaveableContext, michael@0: function(aTarget) { michael@0: UITelemetry.addEvent("action.1", "contextmenu", null, "web_save_media"); michael@0: michael@0: let url = aTarget.currentSrc || aTarget.src; michael@0: let filePickerTitleKey = (aTarget instanceof HTMLVideoElement && michael@0: (aTarget.videoWidth != 0 && aTarget.videoHeight != 0)) michael@0: ? "SaveVideoTitle" : "SaveAudioTitle"; michael@0: // Skipped trying to pull MIME type out of cache for now michael@0: ContentAreaUtils.internalSave(url, null, null, null, null, false, michael@0: filePickerTitleKey, null, aTarget.ownerDocument.documentURIObject, michael@0: aTarget.ownerDocument, true, null); michael@0: }); michael@0: }, michael@0: michael@0: onAppUpdated: function() { michael@0: // initialize the form history and passwords databases on upgrades michael@0: Services.obs.notifyObservers(null, "FormHistory:Init", ""); michael@0: Services.obs.notifyObservers(null, "Passwords:Init", ""); michael@0: michael@0: // Migrate user-set "plugins.click_to_play" pref. See bug 884694. michael@0: // Because the default value is true, a user-set pref means that the pref was set to false. michael@0: if (Services.prefs.prefHasUserValue("plugins.click_to_play")) { michael@0: Services.prefs.setIntPref("plugin.default.state", Ci.nsIPluginTag.STATE_ENABLED); michael@0: Services.prefs.clearUserPref("plugins.click_to_play"); michael@0: } michael@0: }, michael@0: michael@0: shutdown: function shutdown() { michael@0: NativeWindow.uninit(); michael@0: LightWeightThemeWebInstaller.uninit(); michael@0: FormAssistant.uninit(); michael@0: IndexedDB.uninit(); michael@0: ViewportHandler.uninit(); michael@0: XPInstallObserver.uninit(); michael@0: HealthReportStatusListener.uninit(); michael@0: CharacterEncoding.uninit(); michael@0: SearchEngines.uninit(); michael@0: #ifndef MOZ_ANDROID_SYNTHAPKS michael@0: WebappsUI.uninit(); michael@0: #endif michael@0: RemoteDebugger.uninit(); michael@0: Reader.uninit(); michael@0: UserAgentOverrides.uninit(); michael@0: DesktopUserAgent.uninit(); michael@0: ExternalApps.uninit(); michael@0: CastingApps.uninit(); michael@0: Distribution.uninit(); michael@0: Tabs.uninit(); michael@0: }, michael@0: michael@0: // This function returns false during periods where the browser displayed document is michael@0: // different from the browser content document, so user actions and some kinds of viewport michael@0: // updates should be ignored. This period starts when we start loading a new page or michael@0: // switch tabs, and ends when the new browser content document has been drawn and handed michael@0: // off to the compositor. michael@0: isBrowserContentDocumentDisplayed: function() { michael@0: try { michael@0: if (!Services.androidBridge.isContentDocumentDisplayed()) michael@0: return false; michael@0: } catch (e) { michael@0: return false; michael@0: } michael@0: michael@0: let tab = this.selectedTab; michael@0: if (!tab) michael@0: return false; michael@0: return tab.contentDocumentIsDisplayed; michael@0: }, michael@0: michael@0: contentDocumentChanged: function() { michael@0: window.top.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).isFirstPaint = true; michael@0: Services.androidBridge.contentDocumentChanged(); michael@0: }, michael@0: michael@0: get tabs() { michael@0: return this._tabs; michael@0: }, michael@0: michael@0: get selectedTab() { michael@0: return this._selectedTab; michael@0: }, michael@0: michael@0: set selectedTab(aTab) { michael@0: if (this._selectedTab == aTab) michael@0: return; michael@0: michael@0: if (this._selectedTab) { michael@0: this._selectedTab.setActive(false); michael@0: } michael@0: michael@0: this._selectedTab = aTab; michael@0: if (!aTab) michael@0: return; michael@0: michael@0: aTab.setActive(true); michael@0: aTab.setResolution(aTab._zoom, true); michael@0: this.contentDocumentChanged(); michael@0: this.deck.selectedPanel = aTab.browser; michael@0: // Focus the browser so that things like selection will be styled correctly. michael@0: aTab.browser.focus(); michael@0: }, michael@0: michael@0: get selectedBrowser() { michael@0: if (this._selectedTab) michael@0: return this._selectedTab.browser; michael@0: return null; michael@0: }, michael@0: michael@0: getTabForId: function getTabForId(aId) { michael@0: let tabs = this._tabs; michael@0: for (let i=0; i < tabs.length; i++) { michael@0: if (tabs[i].id == aId) michael@0: return tabs[i]; michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: getTabForBrowser: function getTabForBrowser(aBrowser) { michael@0: let tabs = this._tabs; michael@0: for (let i = 0; i < tabs.length; i++) { michael@0: if (tabs[i].browser == aBrowser) michael@0: return tabs[i]; michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: getTabForWindow: function getTabForWindow(aWindow) { michael@0: let tabs = this._tabs; michael@0: for (let i = 0; i < tabs.length; i++) { michael@0: if (tabs[i].browser.contentWindow == aWindow) michael@0: return tabs[i]; michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: getBrowserForWindow: function getBrowserForWindow(aWindow) { michael@0: let tabs = this._tabs; michael@0: for (let i = 0; i < tabs.length; i++) { michael@0: if (tabs[i].browser.contentWindow == aWindow) michael@0: return tabs[i].browser; michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: getBrowserForDocument: function getBrowserForDocument(aDocument) { michael@0: let tabs = this._tabs; michael@0: for (let i = 0; i < tabs.length; i++) { michael@0: if (tabs[i].browser.contentDocument == aDocument) michael@0: return tabs[i].browser; michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: loadURI: function loadURI(aURI, aBrowser, aParams) { michael@0: aBrowser = aBrowser || this.selectedBrowser; michael@0: if (!aBrowser) michael@0: return; michael@0: michael@0: aParams = aParams || {}; michael@0: michael@0: let flags = "flags" in aParams ? aParams.flags : Ci.nsIWebNavigation.LOAD_FLAGS_NONE; michael@0: let postData = ("postData" in aParams && aParams.postData) ? aParams.postData : null; michael@0: let referrerURI = "referrerURI" in aParams ? aParams.referrerURI : null; michael@0: let charset = "charset" in aParams ? aParams.charset : null; michael@0: michael@0: let tab = this.getTabForBrowser(aBrowser); michael@0: if (tab) { michael@0: if ("userSearch" in aParams) tab.userSearch = aParams.userSearch; michael@0: } michael@0: michael@0: try { michael@0: aBrowser.loadURIWithFlags(aURI, flags, referrerURI, charset, postData); michael@0: } catch(e) { michael@0: if (tab) { michael@0: let message = { michael@0: type: "Content:LoadError", michael@0: tabID: tab.id michael@0: }; michael@0: sendMessageToJava(message); michael@0: dump("Handled load error: " + e) michael@0: } michael@0: } michael@0: }, michael@0: michael@0: addTab: function addTab(aURI, aParams) { michael@0: aParams = aParams || {}; michael@0: michael@0: let newTab = new Tab(aURI, aParams); michael@0: this._tabs.push(newTab); michael@0: michael@0: let selected = "selected" in aParams ? aParams.selected : true; michael@0: if (selected) michael@0: this.selectedTab = newTab; michael@0: michael@0: let pinned = "pinned" in aParams ? aParams.pinned : false; michael@0: if (pinned) { michael@0: let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); michael@0: ss.setTabValue(newTab, "appOrigin", aURI); michael@0: } michael@0: michael@0: let evt = document.createEvent("UIEvents"); michael@0: evt.initUIEvent("TabOpen", true, false, window, null); michael@0: newTab.browser.dispatchEvent(evt); michael@0: michael@0: return newTab; michael@0: }, michael@0: michael@0: // Use this method to close a tab from JS. This method sends a message michael@0: // to Java to close the tab in the Java UI (we'll get a Tab:Closed message michael@0: // back from Java when that happens). michael@0: closeTab: function closeTab(aTab) { michael@0: if (!aTab) { michael@0: Cu.reportError("Error trying to close tab (tab doesn't exist)"); michael@0: return; michael@0: } michael@0: michael@0: let message = { michael@0: type: "Tab:Close", michael@0: tabID: aTab.id michael@0: }; michael@0: sendMessageToJava(message); michael@0: }, michael@0: michael@0: #ifdef MOZ_ANDROID_SYNTHAPKS michael@0: _loadWebapp: function(aMessage) { michael@0: michael@0: this._initRuntime(this._startupStatus, aMessage.url, aUrl => { michael@0: this.manifestUrl = aMessage.url; michael@0: this.addTab(aUrl, { title: aMessage.name }); michael@0: }); michael@0: }, michael@0: #endif michael@0: michael@0: // Calling this will update the state in BrowserApp after a tab has been michael@0: // closed in the Java UI. michael@0: _handleTabClosed: function _handleTabClosed(aTab) { michael@0: if (aTab == this.selectedTab) michael@0: this.selectedTab = null; michael@0: michael@0: let evt = document.createEvent("UIEvents"); michael@0: evt.initUIEvent("TabClose", true, false, window, null); michael@0: aTab.browser.dispatchEvent(evt); michael@0: michael@0: aTab.destroy(); michael@0: this._tabs.splice(this._tabs.indexOf(aTab), 1); michael@0: }, michael@0: michael@0: // Use this method to select a tab from JS. This method sends a message michael@0: // to Java to select the tab in the Java UI (we'll get a Tab:Selected message michael@0: // back from Java when that happens). michael@0: selectTab: function selectTab(aTab) { michael@0: if (!aTab) { michael@0: Cu.reportError("Error trying to select tab (tab doesn't exist)"); michael@0: return; michael@0: } michael@0: michael@0: // There's nothing to do if the tab is already selected michael@0: if (aTab == this.selectedTab) michael@0: return; michael@0: michael@0: let message = { michael@0: type: "Tab:Select", michael@0: tabID: aTab.id michael@0: }; michael@0: sendMessageToJava(message); michael@0: }, michael@0: michael@0: /** michael@0: * Gets an open tab with the given URL. michael@0: * michael@0: * @param aURL URL to look for michael@0: * @return the tab with the given URL, or null if no such tab exists michael@0: */ michael@0: getTabWithURL: function getTabWithURL(aURL) { michael@0: let uri = Services.io.newURI(aURL, null, null); michael@0: for (let i = 0; i < this._tabs.length; ++i) { michael@0: let tab = this._tabs[i]; michael@0: if (tab.browser.currentURI.equals(uri)) { michael@0: return tab; michael@0: } michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: /** michael@0: * If a tab with the given URL already exists, that tab is selected. michael@0: * Otherwise, a new tab is opened with the given URL. michael@0: * michael@0: * @param aURL URL to open michael@0: */ michael@0: selectOrOpenTab: function selectOrOpenTab(aURL) { michael@0: let tab = this.getTabWithURL(aURL); michael@0: if (tab == null) { michael@0: this.addTab(aURL); michael@0: } else { michael@0: this.selectTab(tab); michael@0: } michael@0: }, michael@0: michael@0: // This method updates the state in BrowserApp after a tab has been selected michael@0: // in the Java UI. michael@0: _handleTabSelected: function _handleTabSelected(aTab) { michael@0: this.selectedTab = aTab; michael@0: michael@0: let evt = document.createEvent("UIEvents"); michael@0: evt.initUIEvent("TabSelect", true, false, window, null); michael@0: aTab.browser.dispatchEvent(evt); michael@0: }, michael@0: michael@0: quit: function quit() { michael@0: // Figure out if there's at least one other browser window around. michael@0: let lastBrowser = true; michael@0: let e = Services.wm.getEnumerator("navigator:browser"); michael@0: while (e.hasMoreElements() && lastBrowser) { michael@0: let win = e.getNext(); michael@0: if (!win.closed && win != window) michael@0: lastBrowser = false; michael@0: } michael@0: michael@0: if (lastBrowser) { michael@0: // Let everyone know we are closing the last browser window michael@0: let closingCanceled = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool); michael@0: Services.obs.notifyObservers(closingCanceled, "browser-lastwindow-close-requested", null); michael@0: if (closingCanceled.data) michael@0: return; michael@0: michael@0: Services.obs.notifyObservers(null, "browser-lastwindow-close-granted", null); michael@0: } michael@0: michael@0: window.QueryInterface(Ci.nsIDOMChromeWindow).minimize(); michael@0: window.close(); michael@0: }, michael@0: michael@0: saveAsPDF: function saveAsPDF(aBrowser) { michael@0: // Create the final destination file location michael@0: let fileName = ContentAreaUtils.getDefaultFileName(aBrowser.contentTitle, aBrowser.currentURI, null, null); michael@0: fileName = fileName.trim() + ".pdf"; michael@0: michael@0: let dm = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager); michael@0: let downloadsDir = dm.defaultDownloadsDirectory; michael@0: michael@0: let file = downloadsDir.clone(); michael@0: file.append(fileName); michael@0: file.createUnique(file.NORMAL_FILE_TYPE, parseInt("666", 8)); michael@0: michael@0: let printSettings = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(Ci.nsIPrintSettingsService).newPrintSettings; michael@0: printSettings.printSilent = true; michael@0: printSettings.showPrintProgress = false; michael@0: printSettings.printBGImages = true; michael@0: printSettings.printBGColors = true; michael@0: printSettings.printToFile = true; michael@0: printSettings.toFileName = file.path; michael@0: printSettings.printFrameType = Ci.nsIPrintSettings.kFramesAsIs; michael@0: printSettings.outputFormat = Ci.nsIPrintSettings.kOutputFormatPDF; michael@0: michael@0: //XXX we probably need a preference here, the header can be useful michael@0: printSettings.footerStrCenter = ""; michael@0: printSettings.footerStrLeft = ""; michael@0: printSettings.footerStrRight = ""; michael@0: printSettings.headerStrCenter = ""; michael@0: printSettings.headerStrLeft = ""; michael@0: printSettings.headerStrRight = ""; michael@0: michael@0: // Create a valid mimeInfo for the PDF michael@0: let ms = Cc["@mozilla.org/mime;1"].getService(Ci.nsIMIMEService); michael@0: let mimeInfo = ms.getFromTypeAndExtension("application/pdf", "pdf"); michael@0: michael@0: let webBrowserPrint = aBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIWebBrowserPrint); michael@0: michael@0: let cancelable = { michael@0: cancel: function (aReason) { michael@0: webBrowserPrint.cancel(); michael@0: } michael@0: } michael@0: let isPrivate = PrivateBrowsingUtils.isWindowPrivate(aBrowser.contentWindow); michael@0: let download = dm.addDownload(Ci.nsIDownloadManager.DOWNLOAD_TYPE_DOWNLOAD, michael@0: aBrowser.currentURI, michael@0: Services.io.newFileURI(file), "", mimeInfo, michael@0: Date.now() * 1000, null, cancelable, isPrivate); michael@0: michael@0: webBrowserPrint.print(printSettings, download); michael@0: }, michael@0: michael@0: notifyPrefObservers: function(aPref) { michael@0: this._prefObservers[aPref].forEach(function(aRequestId) { michael@0: this.getPreferences(aRequestId, [aPref], 1); michael@0: }, this); michael@0: }, michael@0: michael@0: handlePreferencesRequest: function handlePreferencesRequest(aRequestId, michael@0: aPrefNames, michael@0: aListen) { michael@0: michael@0: let prefs = []; michael@0: michael@0: for (let prefName of aPrefNames) { michael@0: let pref = { michael@0: name: prefName, michael@0: type: "", michael@0: value: null michael@0: }; michael@0: michael@0: if (aListen) { michael@0: if (this._prefObservers[prefName]) michael@0: this._prefObservers[prefName].push(aRequestId); michael@0: else michael@0: this._prefObservers[prefName] = [ aRequestId ]; michael@0: Services.prefs.addObserver(prefName, this, false); michael@0: } michael@0: michael@0: // These pref names are not "real" pref names. michael@0: // They are used in the setting menu, michael@0: // and these are passed when initializing the setting menu. michael@0: switch (prefName) { michael@0: // The plugin pref is actually two separate prefs, so michael@0: // we need to handle it differently michael@0: case "plugin.enable": michael@0: pref.type = "string";// Use a string type for java's ListPreference michael@0: pref.value = PluginHelper.getPluginPreference(); michael@0: prefs.push(pref); michael@0: continue; michael@0: // Handle master password michael@0: case "privacy.masterpassword.enabled": michael@0: pref.type = "bool"; michael@0: pref.value = MasterPassword.enabled; michael@0: prefs.push(pref); michael@0: continue; michael@0: // Handle do-not-track preference michael@0: case "privacy.donottrackheader": michael@0: pref.type = "string"; michael@0: michael@0: let enableDNT = Services.prefs.getBoolPref("privacy.donottrackheader.enabled"); michael@0: if (!enableDNT) { michael@0: pref.value = kDoNotTrackPrefState.NO_PREF; michael@0: } else { michael@0: let dntState = Services.prefs.getIntPref("privacy.donottrackheader.value"); michael@0: pref.value = (dntState === 0) ? kDoNotTrackPrefState.ALLOW_TRACKING : michael@0: kDoNotTrackPrefState.DISALLOW_TRACKING; michael@0: } michael@0: michael@0: prefs.push(pref); michael@0: continue; michael@0: #ifdef MOZ_CRASHREPORTER michael@0: // Crash reporter submit pref must be fetched from nsICrashReporter service. michael@0: case "datareporting.crashreporter.submitEnabled": michael@0: pref.type = "bool"; michael@0: pref.value = CrashReporter.submitReports; michael@0: prefs.push(pref); michael@0: continue; michael@0: #endif michael@0: } michael@0: michael@0: try { michael@0: switch (Services.prefs.getPrefType(prefName)) { michael@0: case Ci.nsIPrefBranch.PREF_BOOL: michael@0: pref.type = "bool"; michael@0: pref.value = Services.prefs.getBoolPref(prefName); michael@0: break; michael@0: case Ci.nsIPrefBranch.PREF_INT: michael@0: pref.type = "int"; michael@0: pref.value = Services.prefs.getIntPref(prefName); michael@0: break; michael@0: case Ci.nsIPrefBranch.PREF_STRING: michael@0: default: michael@0: pref.type = "string"; michael@0: try { michael@0: // Try in case it's a localized string (will throw an exception if not) michael@0: pref.value = Services.prefs.getComplexValue(prefName, Ci.nsIPrefLocalizedString).data; michael@0: } catch (e) { michael@0: pref.value = Services.prefs.getCharPref(prefName); michael@0: } michael@0: break; michael@0: } michael@0: } catch (e) { michael@0: dump("Error reading pref [" + prefName + "]: " + e); michael@0: // preference does not exist; do not send it michael@0: continue; michael@0: } michael@0: michael@0: // Some Gecko preferences use integers or strings to reference michael@0: // state instead of directly representing the value. michael@0: // Since the Java UI uses the type to determine which ui elements michael@0: // to show and how to handle them, we need to normalize these michael@0: // preferences to the correct type. michael@0: switch (prefName) { michael@0: // (string) index for determining which multiple choice value to display. michael@0: case "browser.chrome.titlebarMode": michael@0: case "network.cookie.cookieBehavior": michael@0: case "font.size.inflation.minTwips": michael@0: case "home.sync.updateMode": michael@0: pref.type = "string"; michael@0: pref.value = pref.value.toString(); michael@0: break; michael@0: } michael@0: michael@0: prefs.push(pref); michael@0: } michael@0: michael@0: sendMessageToJava({ michael@0: type: "Preferences:Data", michael@0: requestId: aRequestId, // opaque request identifier, can be any string/int/whatever michael@0: preferences: prefs michael@0: }); michael@0: }, michael@0: michael@0: setPreferences: function setPreferences(aPref) { michael@0: let json = JSON.parse(aPref); michael@0: michael@0: switch (json.name) { michael@0: // The plugin pref is actually two separate prefs, so michael@0: // we need to handle it differently michael@0: case "plugin.enable": michael@0: PluginHelper.setPluginPreference(json.value); michael@0: return; michael@0: michael@0: // MasterPassword pref is not real, we just need take action and leave michael@0: case "privacy.masterpassword.enabled": michael@0: if (MasterPassword.enabled) michael@0: MasterPassword.removePassword(json.value); michael@0: else michael@0: MasterPassword.setPassword(json.value); michael@0: return; michael@0: michael@0: // "privacy.donottrackheader" is not "real" pref name, it's used in the setting menu. michael@0: case "privacy.donottrackheader": michael@0: switch (json.value) { michael@0: // Don't tell anything about tracking me michael@0: case kDoNotTrackPrefState.NO_PREF: michael@0: Services.prefs.setBoolPref("privacy.donottrackheader.enabled", false); michael@0: Services.prefs.clearUserPref("privacy.donottrackheader.value"); michael@0: break; michael@0: // Accept tracking me michael@0: case kDoNotTrackPrefState.ALLOW_TRACKING: michael@0: Services.prefs.setBoolPref("privacy.donottrackheader.enabled", true); michael@0: Services.prefs.setIntPref("privacy.donottrackheader.value", 0); michael@0: break; michael@0: // Not accept tracking me michael@0: case kDoNotTrackPrefState.DISALLOW_TRACKING: michael@0: Services.prefs.setBoolPref("privacy.donottrackheader.enabled", true); michael@0: Services.prefs.setIntPref("privacy.donottrackheader.value", 1); michael@0: break; michael@0: } michael@0: return; michael@0: michael@0: // Enabling or disabling suggestions will prevent future prompts michael@0: case SearchEngines.PREF_SUGGEST_ENABLED: michael@0: Services.prefs.setBoolPref(SearchEngines.PREF_SUGGEST_PROMPTED, true); michael@0: break; michael@0: michael@0: #ifdef MOZ_CRASHREPORTER michael@0: // Crash reporter preference is in a service; set and return. michael@0: case "datareporting.crashreporter.submitEnabled": michael@0: CrashReporter.submitReports = json.value; michael@0: return; michael@0: #endif michael@0: // When sending to Java, we normalized special preferences that use michael@0: // integers and strings to represent booleans. Here, we convert them back michael@0: // to their actual types so we can store them. michael@0: case "browser.chrome.titlebarMode": michael@0: case "network.cookie.cookieBehavior": michael@0: case "font.size.inflation.minTwips": michael@0: case "home.sync.updateMode": michael@0: json.type = "int"; michael@0: json.value = parseInt(json.value); michael@0: break; michael@0: } michael@0: michael@0: switch (json.type) { michael@0: case "bool": michael@0: Services.prefs.setBoolPref(json.name, json.value); michael@0: break; michael@0: case "int": michael@0: Services.prefs.setIntPref(json.name, json.value); michael@0: break; michael@0: default: { michael@0: let pref = Cc["@mozilla.org/pref-localizedstring;1"].createInstance(Ci.nsIPrefLocalizedString); michael@0: pref.data = json.value; michael@0: Services.prefs.setComplexValue(json.name, Ci.nsISupportsString, pref); michael@0: break; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: sanitize: function (aItems) { michael@0: let json = JSON.parse(aItems); michael@0: let success = true; michael@0: michael@0: for (let key in json) { michael@0: if (!json[key]) michael@0: continue; michael@0: michael@0: try { michael@0: switch (key) { michael@0: case "cookies_sessions": michael@0: Sanitizer.clearItem("cookies"); michael@0: Sanitizer.clearItem("sessions"); michael@0: break; michael@0: default: michael@0: Sanitizer.clearItem(key); michael@0: } michael@0: } catch (e) { michael@0: dump("sanitize error: " + e); michael@0: success = false; michael@0: } michael@0: } michael@0: michael@0: sendMessageToJava({ michael@0: type: "Sanitize:Finished", michael@0: success: success michael@0: }); michael@0: }, michael@0: michael@0: getFocusedInput: function(aBrowser, aOnlyInputElements = false) { michael@0: if (!aBrowser) michael@0: return null; michael@0: michael@0: let doc = aBrowser.contentDocument; michael@0: if (!doc) michael@0: return null; michael@0: michael@0: let focused = doc.activeElement; michael@0: while (focused instanceof HTMLFrameElement || focused instanceof HTMLIFrameElement) { michael@0: doc = focused.contentDocument; michael@0: focused = doc.activeElement; michael@0: } michael@0: michael@0: if (focused instanceof HTMLInputElement && focused.mozIsTextField(false)) michael@0: return focused; michael@0: michael@0: if (aOnlyInputElements) michael@0: return null; michael@0: michael@0: if (focused && (focused instanceof HTMLTextAreaElement || focused.isContentEditable)) { michael@0: michael@0: if (focused instanceof HTMLBodyElement) { michael@0: // we are putting focus into a contentEditable frame. scroll the frame into michael@0: // view instead of the contentEditable document contained within, because that michael@0: // results in a better user experience michael@0: focused = focused.ownerDocument.defaultView.frameElement; michael@0: } michael@0: return focused; michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: scrollToFocusedInput: function(aBrowser, aAllowZoom = true) { michael@0: let formHelperMode = Services.prefs.getIntPref("formhelper.mode"); michael@0: if (formHelperMode == kFormHelperModeDisabled) michael@0: return; michael@0: michael@0: let focused = this.getFocusedInput(aBrowser); michael@0: michael@0: if (focused) { michael@0: let shouldZoom = Services.prefs.getBoolPref("formhelper.autozoom"); michael@0: if (formHelperMode == kFormHelperModeDynamic && this.isTablet) michael@0: shouldZoom = false; michael@0: // ZoomHelper.zoomToElement will handle not sending any message if this input is already mostly filling the screen michael@0: ZoomHelper.zoomToElement(focused, -1, false, michael@0: aAllowZoom && shouldZoom && !ViewportHandler.getViewportMetadata(aBrowser.contentWindow).isSpecified); michael@0: } michael@0: }, michael@0: michael@0: observe: function(aSubject, aTopic, aData) { michael@0: let browser = this.selectedBrowser; michael@0: michael@0: switch (aTopic) { michael@0: michael@0: case "Session:Back": michael@0: browser.goBack(); michael@0: break; michael@0: michael@0: case "Session:Forward": michael@0: browser.goForward(); michael@0: break; michael@0: michael@0: case "Session:Reload": { michael@0: let flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY | Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE; michael@0: michael@0: // Check to see if this is a message to enable/disable mixed content blocking. michael@0: if (aData) { michael@0: let allowMixedContent = JSON.parse(aData).allowMixedContent; michael@0: if (allowMixedContent) { michael@0: // Set a flag to disable mixed content blocking. michael@0: flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_MIXED_CONTENT; michael@0: } else { michael@0: // Set mixedContentChannel to null to re-enable mixed content blocking. michael@0: let docShell = browser.webNavigation.QueryInterface(Ci.nsIDocShell); michael@0: docShell.mixedContentChannel = null; michael@0: } michael@0: } michael@0: michael@0: // Try to use the session history to reload so that framesets are michael@0: // handled properly. If the window has no session history, fall back michael@0: // to using the web navigation's reload method. michael@0: let webNav = browser.webNavigation; michael@0: try { michael@0: let sh = webNav.sessionHistory; michael@0: if (sh) michael@0: webNav = sh.QueryInterface(Ci.nsIWebNavigation); michael@0: } catch (e) {} michael@0: webNav.reload(flags); michael@0: break; michael@0: } michael@0: michael@0: case "Session:Stop": michael@0: browser.stop(); michael@0: break; michael@0: michael@0: case "Session:ShowHistory": { michael@0: let data = JSON.parse(aData); michael@0: this.showHistory(data.fromIndex, data.toIndex, data.selIndex); michael@0: break; michael@0: } michael@0: michael@0: case "Tab:Load": { michael@0: let data = JSON.parse(aData); michael@0: michael@0: // Pass LOAD_FLAGS_DISALLOW_INHERIT_OWNER to prevent any loads from michael@0: // inheriting the currently loaded document's principal. michael@0: let flags = Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP | michael@0: Ci.nsIWebNavigation.LOAD_FLAGS_FIXUP_SCHEME_TYPOS; michael@0: if (data.userEntered) { michael@0: flags |= Ci.nsIWebNavigation.LOAD_FLAGS_DISALLOW_INHERIT_OWNER; michael@0: } michael@0: michael@0: let delayLoad = ("delayLoad" in data) ? data.delayLoad : false; michael@0: let params = { michael@0: selected: ("selected" in data) ? data.selected : !delayLoad, michael@0: parentId: ("parentId" in data) ? data.parentId : -1, michael@0: flags: flags, michael@0: tabID: data.tabID, michael@0: isPrivate: (data.isPrivate === true), michael@0: pinned: (data.pinned === true), michael@0: delayLoad: (delayLoad === true), michael@0: desktopMode: (data.desktopMode === true) michael@0: }; michael@0: michael@0: let url = data.url; michael@0: if (data.engine) { michael@0: let engine = Services.search.getEngineByName(data.engine); michael@0: if (engine) { michael@0: params.userSearch = url; michael@0: let submission = engine.getSubmission(url); michael@0: url = submission.uri.spec; michael@0: params.postData = submission.postData; michael@0: } michael@0: } michael@0: michael@0: if (data.newTab) { michael@0: this.addTab(url, params); michael@0: } else { michael@0: if (data.tabId) { michael@0: // Use a specific browser instead of the selected browser, if it exists michael@0: let specificBrowser = this.getTabForId(data.tabId).browser; michael@0: if (specificBrowser) michael@0: browser = specificBrowser; michael@0: } michael@0: this.loadURI(url, browser, params); michael@0: } michael@0: break; michael@0: } michael@0: michael@0: case "Tab:Selected": michael@0: this._handleTabSelected(this.getTabForId(parseInt(aData))); michael@0: break; michael@0: michael@0: case "Tab:Closed": michael@0: this._handleTabClosed(this.getTabForId(parseInt(aData))); michael@0: break; michael@0: michael@0: case "keyword-search": michael@0: // This event refers to a search via the URL bar, not a bookmarks michael@0: // keyword search. Note that this code assumes that the user can only michael@0: // perform a keyword search on the selected tab. michael@0: this.selectedTab.userSearch = aData; michael@0: michael@0: let engine = aSubject.QueryInterface(Ci.nsISearchEngine); michael@0: sendMessageToJava({ michael@0: type: "Search:Keyword", michael@0: identifier: engine.identifier, michael@0: name: engine.name, michael@0: }); michael@0: break; michael@0: michael@0: case "Browser:Quit": michael@0: this.quit(); michael@0: break; michael@0: michael@0: case "SaveAs:PDF": michael@0: this.saveAsPDF(browser); michael@0: break; michael@0: michael@0: case "Preferences:Set": michael@0: this.setPreferences(aData); michael@0: break; michael@0: michael@0: case "ScrollTo:FocusedInput": michael@0: // these messages come from a change in the viewable area and not user interaction michael@0: // we allow scrolling to the selected input, but not zooming the page michael@0: this.scrollToFocusedInput(browser, false); michael@0: break; michael@0: michael@0: case "Sanitize:ClearData": michael@0: this.sanitize(aData); michael@0: break; michael@0: michael@0: case "FullScreen:Exit": michael@0: browser.contentDocument.mozCancelFullScreen(); michael@0: break; michael@0: michael@0: case "Viewport:Change": michael@0: if (this.isBrowserContentDocumentDisplayed()) michael@0: this.selectedTab.setViewport(JSON.parse(aData)); michael@0: break; michael@0: michael@0: case "Viewport:Flush": michael@0: this.contentDocumentChanged(); michael@0: break; michael@0: michael@0: case "Passwords:Init": { michael@0: let storage = Cc["@mozilla.org/login-manager/storage/mozStorage;1"]. michael@0: getService(Ci.nsILoginManagerStorage); michael@0: storage.init(); michael@0: Services.obs.removeObserver(this, "Passwords:Init"); michael@0: break; michael@0: } michael@0: michael@0: case "FormHistory:Init": { michael@0: // Force creation/upgrade of formhistory.sqlite michael@0: FormHistory.count({}); michael@0: Services.obs.removeObserver(this, "FormHistory:Init"); michael@0: break; michael@0: } michael@0: michael@0: case "sessionstore-state-purge-complete": michael@0: sendMessageToJava({ type: "Session:StatePurged" }); michael@0: break; michael@0: michael@0: case "gather-telemetry": michael@0: sendMessageToJava({ type: "Telemetry:Gather" }); michael@0: break; michael@0: michael@0: case "Viewport:FixedMarginsChanged": michael@0: gViewportMargins = JSON.parse(aData); michael@0: this.selectedTab.updateViewportSize(gScreenWidth); michael@0: break; michael@0: michael@0: case "nsPref:changed": michael@0: this.notifyPrefObservers(aData); michael@0: break; michael@0: michael@0: #ifdef MOZ_ANDROID_SYNTHAPKS michael@0: case "webapps-runtime-install": michael@0: WebappManager.install(JSON.parse(aData), aSubject); michael@0: break; michael@0: michael@0: case "webapps-runtime-install-package": michael@0: WebappManager.installPackage(JSON.parse(aData), aSubject); michael@0: break; michael@0: michael@0: case "webapps-ask-install": michael@0: WebappManager.askInstall(JSON.parse(aData)); michael@0: break; michael@0: michael@0: case "webapps-launch": { michael@0: WebappManager.launch(JSON.parse(aData)); michael@0: break; michael@0: } michael@0: michael@0: case "webapps-uninstall": { michael@0: WebappManager.uninstall(JSON.parse(aData)); michael@0: break; michael@0: } michael@0: michael@0: case "Webapps:AutoInstall": michael@0: WebappManager.autoInstall(JSON.parse(aData)); michael@0: break; michael@0: michael@0: case "Webapps:Load": michael@0: this._loadWebapp(JSON.parse(aData)); michael@0: break; michael@0: michael@0: case "Webapps:AutoUninstall": michael@0: WebappManager.autoUninstall(JSON.parse(aData)); michael@0: break; michael@0: #endif michael@0: michael@0: case "Locale:Changed": michael@0: // The value provided to Locale:Changed should be a BCP47 language tag michael@0: // understood by Gecko -- for example, "es-ES" or "de". michael@0: console.log("Locale:Changed: " + aData); michael@0: michael@0: // TODO: do we need to be more nuanced here -- e.g., checking for the michael@0: // OS locale -- or should it always be false on Fennec? michael@0: Services.prefs.setBoolPref("intl.locale.matchOS", false); michael@0: Services.prefs.setCharPref("general.useragent.locale", aData); michael@0: break; michael@0: michael@0: default: michael@0: dump('BrowserApp.observe: unexpected topic "' + aTopic + '"\n'); michael@0: break; michael@0: michael@0: } michael@0: }, michael@0: michael@0: get defaultBrowserWidth() { michael@0: delete this.defaultBrowserWidth; michael@0: let width = Services.prefs.getIntPref("browser.viewport.desktopWidth"); michael@0: return this.defaultBrowserWidth = width; michael@0: }, michael@0: michael@0: // nsIAndroidBrowserApp michael@0: getBrowserTab: function(tabId) { michael@0: return this.getTabForId(tabId); michael@0: }, michael@0: michael@0: getUITelemetryObserver: function() { michael@0: return UITelemetry; michael@0: }, michael@0: michael@0: getPreferences: function getPreferences(requestId, prefNames, count) { michael@0: this.handlePreferencesRequest(requestId, prefNames, false); michael@0: }, michael@0: michael@0: observePreferences: function observePreferences(requestId, prefNames, count) { michael@0: this.handlePreferencesRequest(requestId, prefNames, true); michael@0: }, michael@0: michael@0: removePreferenceObservers: function removePreferenceObservers(aRequestId) { michael@0: let newPrefObservers = []; michael@0: for (let prefName in this._prefObservers) { michael@0: let requestIds = this._prefObservers[prefName]; michael@0: // Remove the requestID from the preference handlers michael@0: let i = requestIds.indexOf(aRequestId); michael@0: if (i >= 0) { michael@0: requestIds.splice(i, 1); michael@0: } michael@0: michael@0: // If there are no more request IDs, remove the observer michael@0: if (requestIds.length == 0) { michael@0: Services.prefs.removeObserver(prefName, this); michael@0: } else { michael@0: newPrefObservers[prefName] = requestIds; michael@0: } michael@0: } michael@0: this._prefObservers = newPrefObservers; michael@0: }, michael@0: michael@0: // This method will print a list from fromIndex to toIndex, optionally michael@0: // selecting selIndex(if fromIndex<=selIndex<=toIndex) michael@0: showHistory: function(fromIndex, toIndex, selIndex) { michael@0: let browser = this.selectedBrowser; michael@0: let hist = browser.sessionHistory; michael@0: let listitems = []; michael@0: for (let i = toIndex; i >= fromIndex; i--) { michael@0: let entry = hist.getEntryAtIndex(i, false); michael@0: let item = { michael@0: label: entry.title || entry.URI.spec, michael@0: selected: (i == selIndex) michael@0: }; michael@0: listitems.push(item); michael@0: } michael@0: michael@0: let p = new Prompt({ michael@0: window: browser.contentWindow michael@0: }).setSingleChoiceItems(listitems).show(function(data) { michael@0: let selected = data.button; michael@0: if (selected == -1) michael@0: return; michael@0: michael@0: browser.gotoIndex(toIndex-selected); michael@0: }); michael@0: }, michael@0: }; michael@0: michael@0: var NativeWindow = { michael@0: init: function() { michael@0: Services.obs.addObserver(this, "Menu:Clicked", false); michael@0: Services.obs.addObserver(this, "PageActions:Clicked", false); michael@0: Services.obs.addObserver(this, "PageActions:LongClicked", false); michael@0: Services.obs.addObserver(this, "Doorhanger:Reply", false); michael@0: Services.obs.addObserver(this, "Toast:Click", false); michael@0: Services.obs.addObserver(this, "Toast:Hidden", false); michael@0: this.contextmenus.init(); michael@0: }, michael@0: michael@0: uninit: function() { michael@0: Services.obs.removeObserver(this, "Menu:Clicked"); michael@0: Services.obs.removeObserver(this, "PageActions:Clicked"); michael@0: Services.obs.removeObserver(this, "PageActions:LongClicked"); michael@0: Services.obs.removeObserver(this, "Doorhanger:Reply"); michael@0: Services.obs.removeObserver(this, "Toast:Click", false); michael@0: Services.obs.removeObserver(this, "Toast:Hidden", false); michael@0: this.contextmenus.uninit(); michael@0: }, michael@0: michael@0: loadDex: function(zipFile, implClass) { michael@0: sendMessageToJava({ michael@0: type: "Dex:Load", michael@0: zipfile: zipFile, michael@0: impl: implClass || "Main" michael@0: }); michael@0: }, michael@0: michael@0: unloadDex: function(zipFile) { michael@0: sendMessageToJava({ michael@0: type: "Dex:Unload", michael@0: zipfile: zipFile michael@0: }); michael@0: }, michael@0: michael@0: toast: { michael@0: _callbacks: {}, michael@0: show: function(aMessage, aDuration, aOptions) { michael@0: let msg = { michael@0: type: "Toast:Show", michael@0: message: aMessage, michael@0: duration: aDuration michael@0: }; michael@0: michael@0: if (aOptions && aOptions.button) { michael@0: msg.button = { michael@0: label: aOptions.button.label, michael@0: id: uuidgen.generateUUID().toString(), michael@0: // If the caller specified a button, make sure we convert any chrome urls michael@0: // to jar:jar urls so that the frontend can show them michael@0: icon: aOptions.button.icon ? resolveGeckoURI(aOptions.button.icon) : null, michael@0: }; michael@0: this._callbacks[msg.button.id] = aOptions.button.callback; michael@0: } michael@0: michael@0: sendMessageToJava(msg); michael@0: } michael@0: }, michael@0: michael@0: pageactions: { michael@0: _items: { }, michael@0: add: function(aOptions) { michael@0: let id = uuidgen.generateUUID().toString(); michael@0: sendMessageToJava({ michael@0: type: "PageActions:Add", michael@0: id: id, michael@0: title: aOptions.title, michael@0: icon: resolveGeckoURI(aOptions.icon), michael@0: important: "important" in aOptions ? aOptions.important : false michael@0: }); michael@0: this._items[id] = { michael@0: clickCallback: aOptions.clickCallback, michael@0: longClickCallback: aOptions.longClickCallback michael@0: }; michael@0: return id; michael@0: }, michael@0: remove: function(id) { michael@0: sendMessageToJava({ michael@0: type: "PageActions:Remove", michael@0: id: id michael@0: }); michael@0: delete this._items[id]; michael@0: } michael@0: }, michael@0: michael@0: menu: { michael@0: _callbacks: [], michael@0: _menuId: 1, michael@0: toolsMenuID: -1, michael@0: add: function() { michael@0: let options; michael@0: if (arguments.length == 1) { michael@0: options = arguments[0]; michael@0: } else if (arguments.length == 3) { michael@0: options = { michael@0: name: arguments[0], michael@0: icon: arguments[1], michael@0: callback: arguments[2] michael@0: }; michael@0: } else { michael@0: throw "Incorrect number of parameters"; michael@0: } michael@0: michael@0: options.type = "Menu:Add"; michael@0: options.id = this._menuId; michael@0: michael@0: sendMessageToJava(options); michael@0: this._callbacks[this._menuId] = options.callback; michael@0: this._menuId++; michael@0: return this._menuId - 1; michael@0: }, michael@0: michael@0: remove: function(aId) { michael@0: sendMessageToJava({ type: "Menu:Remove", id: aId }); michael@0: }, michael@0: michael@0: update: function(aId, aOptions) { michael@0: if (!aOptions) michael@0: return; michael@0: michael@0: sendMessageToJava({ michael@0: type: "Menu:Update", michael@0: id: aId, michael@0: options: aOptions michael@0: }); michael@0: } michael@0: }, michael@0: michael@0: doorhanger: { michael@0: _callbacks: {}, michael@0: _callbacksId: 0, michael@0: _promptId: 0, michael@0: michael@0: /** michael@0: * @param aOptions michael@0: * An options JavaScript object holding additional properties for the michael@0: * notification. The following properties are currently supported: michael@0: * persistence: An integer. The notification will not automatically michael@0: * dismiss for this many page loads. If persistence is set michael@0: * to -1, the doorhanger will never automatically dismiss. michael@0: * persistWhileVisible: michael@0: * A boolean. If true, a visible notification will always michael@0: * persist across location changes. michael@0: * timeout: A time in milliseconds. The notification will not michael@0: * automatically dismiss before this time. michael@0: * checkbox: A string to appear next to a checkbox under the notification michael@0: * message. The button callback functions will be called with michael@0: * the checked state as an argument. michael@0: */ michael@0: show: function(aMessage, aValue, aButtons, aTabID, aOptions) { michael@0: if (aButtons == null) { michael@0: aButtons = []; michael@0: } michael@0: michael@0: aButtons.forEach((function(aButton) { michael@0: this._callbacks[this._callbacksId] = { cb: aButton.callback, prompt: this._promptId }; michael@0: aButton.callback = this._callbacksId; michael@0: this._callbacksId++; michael@0: }).bind(this)); michael@0: michael@0: this._promptId++; michael@0: let json = { michael@0: type: "Doorhanger:Add", michael@0: message: aMessage, michael@0: value: aValue, michael@0: buttons: aButtons, michael@0: // use the current tab if none is provided michael@0: tabID: aTabID || BrowserApp.selectedTab.id, michael@0: options: aOptions || {} michael@0: }; michael@0: sendMessageToJava(json); michael@0: }, michael@0: michael@0: hide: function(aValue, aTabID) { michael@0: sendMessageToJava({ michael@0: type: "Doorhanger:Remove", michael@0: value: aValue, michael@0: tabID: aTabID michael@0: }); michael@0: } michael@0: }, michael@0: michael@0: observe: function(aSubject, aTopic, aData) { michael@0: if (aTopic == "Menu:Clicked") { michael@0: if (this.menu._callbacks[aData]) michael@0: this.menu._callbacks[aData](); michael@0: } else if (aTopic == "PageActions:Clicked") { michael@0: if (this.pageactions._items[aData].clickCallback) michael@0: this.pageactions._items[aData].clickCallback(); michael@0: } else if (aTopic == "PageActions:LongClicked") { michael@0: if (this.pageactions._items[aData].longClickCallback) michael@0: this.pageactions._items[aData].longClickCallback(); michael@0: } else if (aTopic == "Toast:Click") { michael@0: if (this.toast._callbacks[aData]) { michael@0: this.toast._callbacks[aData](); michael@0: delete this.toast._callbacks[aData]; michael@0: } michael@0: } else if (aTopic == "Toast:Hidden") { michael@0: if (this.toast._callbacks[aData]) michael@0: delete this.toast._callbacks[aData]; michael@0: } else if (aTopic == "Doorhanger:Reply") { michael@0: let data = JSON.parse(aData); michael@0: let reply_id = data["callback"]; michael@0: michael@0: if (this.doorhanger._callbacks[reply_id]) { michael@0: // Pass the value of the optional checkbox to the callback michael@0: let checked = data["checked"]; michael@0: this.doorhanger._callbacks[reply_id].cb(checked, data.inputs); michael@0: michael@0: let prompt = this.doorhanger._callbacks[reply_id].prompt; michael@0: for (let id in this.doorhanger._callbacks) { michael@0: if (this.doorhanger._callbacks[id].prompt == prompt) { michael@0: delete this.doorhanger._callbacks[id]; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: contextmenus: { michael@0: items: {}, // a list of context menu items that we may show michael@0: DEFAULT_HTML5_ORDER: -1, // Sort order for HTML5 context menu items michael@0: michael@0: init: function() { michael@0: Services.obs.addObserver(this, "Gesture:LongPress", false); michael@0: }, michael@0: michael@0: uninit: function() { michael@0: Services.obs.removeObserver(this, "Gesture:LongPress"); michael@0: }, michael@0: michael@0: add: function() { michael@0: let args; michael@0: if (arguments.length == 1) { michael@0: args = arguments[0]; michael@0: } else if (arguments.length == 3) { michael@0: args = { michael@0: label : arguments[0], michael@0: selector: arguments[1], michael@0: callback: arguments[2] michael@0: }; michael@0: } else { michael@0: throw "Incorrect number of parameters"; michael@0: } michael@0: michael@0: if (!args.label) michael@0: throw "Menu items must have a name"; michael@0: michael@0: let cmItem = new ContextMenuItem(args); michael@0: this.items[cmItem.id] = cmItem; michael@0: return cmItem.id; michael@0: }, michael@0: michael@0: remove: function(aId) { michael@0: delete this.items[aId]; michael@0: }, michael@0: michael@0: SelectorContext: function(aSelector) { michael@0: return { michael@0: matches: function(aElt) { michael@0: if (aElt.mozMatchesSelector) michael@0: return aElt.mozMatchesSelector(aSelector); michael@0: return false; michael@0: } michael@0: }; michael@0: }, michael@0: michael@0: linkOpenableNonPrivateContext: { michael@0: matches: function linkOpenableNonPrivateContextMatches(aElement) { michael@0: let doc = aElement.ownerDocument; michael@0: if (!doc || PrivateBrowsingUtils.isWindowPrivate(doc.defaultView)) { michael@0: return false; michael@0: } michael@0: michael@0: return NativeWindow.contextmenus.linkOpenableContext.matches(aElement); michael@0: } michael@0: }, michael@0: michael@0: linkOpenableContext: { michael@0: matches: function linkOpenableContextMatches(aElement) { michael@0: let uri = NativeWindow.contextmenus._getLink(aElement); michael@0: if (uri) { michael@0: let scheme = uri.scheme; michael@0: let dontOpen = /^(javascript|mailto|news|snews|tel)$/; michael@0: return (scheme && !dontOpen.test(scheme)); michael@0: } michael@0: return false; michael@0: } michael@0: }, michael@0: michael@0: linkCopyableContext: { michael@0: matches: function linkCopyableContextMatches(aElement) { michael@0: let uri = NativeWindow.contextmenus._getLink(aElement); michael@0: if (uri) { michael@0: let scheme = uri.scheme; michael@0: let dontCopy = /^(mailto|tel)$/; michael@0: return (scheme && !dontCopy.test(scheme)); michael@0: } michael@0: return false; michael@0: } michael@0: }, michael@0: michael@0: linkShareableContext: { michael@0: matches: function linkShareableContextMatches(aElement) { michael@0: let uri = NativeWindow.contextmenus._getLink(aElement); michael@0: if (uri) { michael@0: let scheme = uri.scheme; michael@0: let dontShare = /^(about|chrome|file|javascript|mailto|resource|tel)$/; michael@0: return (scheme && !dontShare.test(scheme)); michael@0: } michael@0: return false; michael@0: } michael@0: }, michael@0: michael@0: linkBookmarkableContext: { michael@0: matches: function linkBookmarkableContextMatches(aElement) { michael@0: let uri = NativeWindow.contextmenus._getLink(aElement); michael@0: if (uri) { michael@0: let scheme = uri.scheme; michael@0: let dontBookmark = /^(mailto|tel)$/; michael@0: return (scheme && !dontBookmark.test(scheme)); michael@0: } michael@0: return false; michael@0: } michael@0: }, michael@0: michael@0: emailLinkContext: { michael@0: matches: function emailLinkContextMatches(aElement) { michael@0: let uri = NativeWindow.contextmenus._getLink(aElement); michael@0: if (uri) michael@0: return uri.schemeIs("mailto"); michael@0: return false; michael@0: } michael@0: }, michael@0: michael@0: phoneNumberLinkContext: { michael@0: matches: function phoneNumberLinkContextMatches(aElement) { michael@0: let uri = NativeWindow.contextmenus._getLink(aElement); michael@0: if (uri) michael@0: return uri.schemeIs("tel"); michael@0: return false; michael@0: } michael@0: }, michael@0: michael@0: imageLocationCopyableContext: { michael@0: matches: function imageLinkCopyableContextMatches(aElement) { michael@0: return (aElement instanceof Ci.nsIImageLoadingContent && aElement.currentURI); michael@0: } michael@0: }, michael@0: michael@0: imageSaveableContext: { michael@0: matches: function imageSaveableContextMatches(aElement) { michael@0: if (aElement instanceof Ci.nsIImageLoadingContent && aElement.currentURI) { michael@0: // The image must be loaded to allow saving michael@0: let request = aElement.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST); michael@0: return (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE)); michael@0: } michael@0: return false; michael@0: } michael@0: }, michael@0: michael@0: mediaSaveableContext: { michael@0: matches: function mediaSaveableContextMatches(aElement) { michael@0: return (aElement instanceof HTMLVideoElement || michael@0: aElement instanceof HTMLAudioElement); michael@0: } michael@0: }, michael@0: michael@0: mediaContext: function(aMode) { michael@0: return { michael@0: matches: function(aElt) { michael@0: if (aElt instanceof Ci.nsIDOMHTMLMediaElement) { michael@0: let hasError = aElt.error != null || aElt.networkState == aElt.NETWORK_NO_SOURCE; michael@0: if (hasError) michael@0: return false; michael@0: michael@0: let paused = aElt.paused || aElt.ended; michael@0: if (paused && aMode == "media-paused") michael@0: return true; michael@0: if (!paused && aMode == "media-playing") michael@0: return true; michael@0: let controls = aElt.controls; michael@0: if (!controls && aMode == "media-hidingcontrols") michael@0: return true; michael@0: michael@0: let muted = aElt.muted; michael@0: if (muted && aMode == "media-muted") michael@0: return true; michael@0: else if (!muted && aMode == "media-unmuted") michael@0: return true; michael@0: } michael@0: return false; michael@0: } michael@0: }; michael@0: }, michael@0: michael@0: /* Holds a WeakRef to the original target element this context menu was shown for. michael@0: * Most API's will have to walk up the tree from this node to find the correct element michael@0: * to act on michael@0: */ michael@0: get _target() { michael@0: if (this._targetRef) michael@0: return this._targetRef.get(); michael@0: return null; michael@0: }, michael@0: michael@0: set _target(aTarget) { michael@0: if (aTarget) michael@0: this._targetRef = Cu.getWeakReference(aTarget); michael@0: else this._targetRef = null; michael@0: }, michael@0: michael@0: get defaultContext() { michael@0: delete this.defaultContext; michael@0: return this.defaultContext = Strings.browser.GetStringFromName("browser.menu.context.default"); michael@0: }, michael@0: michael@0: /* Gets menuitems for an arbitrary node michael@0: * Parameters: michael@0: * element - The element to look at. If this element has a contextmenu attribute, the michael@0: * corresponding contextmenu will be used. michael@0: */ michael@0: _getHTMLContextMenuItemsForElement: function(element) { michael@0: let htmlMenu = element.contextMenu; michael@0: if (!htmlMenu) { michael@0: return []; michael@0: } michael@0: michael@0: htmlMenu.QueryInterface(Components.interfaces.nsIHTMLMenu); michael@0: htmlMenu.sendShowEvent(); michael@0: michael@0: return this._getHTMLContextMenuItemsForMenu(htmlMenu, element); michael@0: }, michael@0: michael@0: /* Add a menuitem for an HTML node michael@0: * Parameters: michael@0: * menu - The element to iterate through for menuitems michael@0: * target - The target element these context menu items are attached to michael@0: */ michael@0: _getHTMLContextMenuItemsForMenu: function(menu, target) { michael@0: let items = []; michael@0: for (let i = 0; i < menu.childNodes.length; i++) { michael@0: let elt = menu.childNodes[i]; michael@0: if (!elt.label) michael@0: continue; michael@0: michael@0: items.push(new HTMLContextMenuItem(elt, target)); michael@0: } michael@0: michael@0: return items; michael@0: }, michael@0: michael@0: // Searches the current list of menuitems to show for any that match this id michael@0: _findMenuItem: function(aId) { michael@0: if (!this.menus) { michael@0: return null; michael@0: } michael@0: michael@0: for (let context in this.menus) { michael@0: let menu = this.menus[context]; michael@0: for (let i = 0; i < menu.length; i++) { michael@0: if (menu[i].id === aId) { michael@0: return menu[i]; michael@0: } michael@0: } michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: // Returns true if there are any context menu items to show michael@0: shouldShow: function() { michael@0: for (let context in this.menus) { michael@0: let menu = this.menus[context]; michael@0: if (menu.length > 0) { michael@0: return true; michael@0: } michael@0: } michael@0: return false; michael@0: }, michael@0: michael@0: /* Returns a label to be shown in a tabbed ui if there are multiple "contexts". For instance, if this michael@0: * is an image inside an tag, we may have a "link" context and an "image" one. michael@0: */ michael@0: _getContextType: function(element) { michael@0: // For anchor nodes, we try to use the scheme to pick a string michael@0: if (element instanceof Ci.nsIDOMHTMLAnchorElement) { michael@0: let uri = this.makeURI(this._getLinkURL(element)); michael@0: try { michael@0: return Strings.browser.GetStringFromName("browser.menu.context." + uri.scheme); michael@0: } catch(ex) { } michael@0: } michael@0: michael@0: // Otherwise we try the nodeName michael@0: try { michael@0: return Strings.browser.GetStringFromName("browser.menu.context." + element.nodeName.toLowerCase()); michael@0: } catch(ex) { } michael@0: michael@0: // Fallback to the default michael@0: return this.defaultContext; michael@0: }, michael@0: michael@0: // Adds context menu items added through the add-on api michael@0: _getNativeContextMenuItems: function(element, x, y) { michael@0: let res = []; michael@0: for (let itemId of Object.keys(this.items)) { michael@0: let item = this.items[itemId]; michael@0: michael@0: if (!this._findMenuItem(item.id) && item.matches(element, x, y)) { michael@0: res.push(item); michael@0: } michael@0: } michael@0: michael@0: return res; michael@0: }, michael@0: michael@0: /* Checks if there are context menu items to show, and if it finds them michael@0: * sends a contextmenu event to content. We also send showing events to michael@0: * any html5 context menus we are about to show, and fire some local notifications michael@0: * for chrome consumers to do lazy menuitem construction michael@0: */ michael@0: _sendToContent: function(x, y) { michael@0: let target = BrowserEventHandler._highlightElement || ElementTouchHelper.elementFromPoint(x, y); michael@0: if (!target) michael@0: target = ElementTouchHelper.anyElementFromPoint(x, y); michael@0: michael@0: if (!target) michael@0: return; michael@0: michael@0: this._target = target; michael@0: michael@0: Services.obs.notifyObservers(null, "before-build-contextmenu", ""); michael@0: this._buildMenu(x, y); michael@0: michael@0: // only send the contextmenu event to content if we are planning to show a context menu (i.e. not on every long tap) michael@0: if (this.shouldShow()) { michael@0: let event = target.ownerDocument.createEvent("MouseEvent"); michael@0: event.initMouseEvent("contextmenu", true, true, target.defaultView, michael@0: 0, x, y, x, y, false, false, false, false, michael@0: 0, null); michael@0: target.ownerDocument.defaultView.addEventListener("contextmenu", this, false); michael@0: target.dispatchEvent(event); michael@0: } else { michael@0: this.menus = null; michael@0: Services.obs.notifyObservers({target: target, x: x, y: y}, "context-menu-not-shown", ""); michael@0: michael@0: if (SelectionHandler.canSelect(target)) { michael@0: if (!SelectionHandler.startSelection(target, { michael@0: mode: SelectionHandler.SELECT_AT_POINT, michael@0: x: x, michael@0: y: y michael@0: })) { michael@0: SelectionHandler.attachCaret(target); michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: michael@0: // Returns a title for a context menu. If no title attribute exists, will fall back to looking for a url michael@0: _getTitle: function(node) { michael@0: if (node.hasAttribute && node.hasAttribute("title")) { michael@0: return node.getAttribute("title"); michael@0: } michael@0: return this._getUrl(node); michael@0: }, michael@0: michael@0: // Returns a url associated with a node michael@0: _getUrl: function(node) { michael@0: if ((node instanceof Ci.nsIDOMHTMLAnchorElement && node.href) || michael@0: (node instanceof Ci.nsIDOMHTMLAreaElement && node.href)) { michael@0: return this._getLinkURL(node); michael@0: } else if (node instanceof Ci.nsIImageLoadingContent && node.currentURI) { michael@0: return node.currentURI.spec; michael@0: } else if (node instanceof Ci.nsIDOMHTMLMediaElement) { michael@0: return (node.currentSrc || node.src); michael@0: } michael@0: michael@0: return ""; michael@0: }, michael@0: michael@0: // Adds an array of menuitems to the current list of items to show, in the correct context michael@0: _addMenuItems: function(items, context) { michael@0: if (!this.menus[context]) { michael@0: this.menus[context] = []; michael@0: } michael@0: this.menus[context] = this.menus[context].concat(items); michael@0: }, michael@0: michael@0: /* Does the basic work of building a context menu to show. Will combine HTML and Native michael@0: * context menus items, as well as sorting menuitems into different menus based on context. michael@0: */ michael@0: _buildMenu: function(x, y) { michael@0: // now walk up the tree and for each node look for any context menu items that apply michael@0: let element = this._target; michael@0: michael@0: // this.menus holds a hashmap of "contexts" to menuitems associated with that context michael@0: // For instance, if the user taps an image inside a link, we'll have something like: michael@0: // { michael@0: // link: [ ContextMenuItem, ContextMenuItem ] michael@0: // image: [ ContextMenuItem, ContextMenuItem ] michael@0: // } michael@0: this.menus = {}; michael@0: michael@0: while (element) { michael@0: let context = this._getContextType(element); michael@0: michael@0: // First check for any html5 context menus that might exist... michael@0: var items = this._getHTMLContextMenuItemsForElement(element); michael@0: if (items.length > 0) { michael@0: this._addMenuItems(items, context); michael@0: } michael@0: michael@0: // then check for any context menu items registered in the ui. michael@0: items = this._getNativeContextMenuItems(element, x, y); michael@0: if (items.length > 0) { michael@0: this._addMenuItems(items, context); michael@0: } michael@0: michael@0: // walk up the tree and find more items to show michael@0: element = element.parentNode; michael@0: } michael@0: }, michael@0: michael@0: // Actually shows the native context menu by passing a list of context menu items to michael@0: // show to the Java. michael@0: _show: function(aEvent) { michael@0: let popupNode = this._target; michael@0: this._target = null; michael@0: if (aEvent.defaultPrevented || !popupNode) { michael@0: return; michael@0: } michael@0: this._innerShow(popupNode, aEvent.clientX, aEvent.clientY); michael@0: }, michael@0: michael@0: // Walks the DOM tree to find a title from a node michael@0: _findTitle: function(node) { michael@0: let title = ""; michael@0: while(node && !title) { michael@0: title = this._getTitle(node); michael@0: node = node.parentNode; michael@0: } michael@0: return title; michael@0: }, michael@0: michael@0: /* Reformats the list of menus to show into an object that can be sent to Prompt.jsm michael@0: * If there is one menu, will return a flat array of menuitems. If there are multiple michael@0: * menus, will return an array with appropriate tabs/items inside it. i.e. : michael@0: * [ michael@0: * { label: "link", items: [...] }, michael@0: * { label: "image", items: [...] } michael@0: * ] michael@0: */ michael@0: _reformatList: function(target) { michael@0: let contexts = Object.keys(this.menus); michael@0: michael@0: if (contexts.length === 1) { michael@0: // If there's only one context, we'll only show a single flat single select list michael@0: return this._reformatMenuItems(target, this.menus[contexts[0]]); michael@0: } michael@0: michael@0: // If there are multiple contexts, we'll only show a tabbed ui with multiple lists michael@0: return this._reformatListAsTabs(target, this.menus); michael@0: }, michael@0: michael@0: /* Reformats the list of menus to show into an object that can be sent to Prompt.jsm's michael@0: * addTabs method. i.e. : michael@0: * { link: [...], image: [...] } becomes michael@0: * [ { label: "link", items: [...] } ] michael@0: * michael@0: * Also reformats items and resolves any parmaeters that aren't known until display time michael@0: * (for instance Helper app menu items adjust their title to reflect what Helper App can be used for this link). michael@0: */ michael@0: _reformatListAsTabs: function(target, menus) { michael@0: let itemArray = []; michael@0: michael@0: // Sort the keys so that "link" is always first michael@0: let contexts = Object.keys(this.menus); michael@0: contexts.sort((context1, context2) => { michael@0: if (context1 === this.defaultContext) { michael@0: return -1; michael@0: } else if (context2 === this.defaultContext) { michael@0: return 1; michael@0: } michael@0: return 0; michael@0: }); michael@0: michael@0: contexts.forEach(context => { michael@0: itemArray.push({ michael@0: label: context, michael@0: items: this._reformatMenuItems(target, menus[context]) michael@0: }); michael@0: }); michael@0: michael@0: return itemArray; michael@0: }, michael@0: michael@0: /* Reformats an array of ContextMenuItems into an array that can be handled by Prompt.jsm. Also reformats items michael@0: * and resolves any parmaeters that aren't known until display time michael@0: * (for instance Helper app menu items adjust their title to reflect what Helper App can be used for this link). michael@0: */ michael@0: _reformatMenuItems: function(target, menuitems) { michael@0: let itemArray = []; michael@0: michael@0: for (let i = 0; i < menuitems.length; i++) { michael@0: let t = target; michael@0: while(t) { michael@0: if (menuitems[i].matches(t)) { michael@0: let val = menuitems[i].getValue(t); michael@0: michael@0: // hidden menu items will return null from getValue michael@0: if (val) { michael@0: itemArray.push(val); michael@0: break; michael@0: } michael@0: } michael@0: michael@0: t = t.parentNode; michael@0: } michael@0: } michael@0: michael@0: return itemArray; michael@0: }, michael@0: michael@0: // Called where we're finally ready to actually show the contextmenu. Sorts the items and shows a prompt. michael@0: _innerShow: function(target, x, y) { michael@0: Haptic.performSimpleAction(Haptic.LongPress); michael@0: michael@0: // spin through the tree looking for a title for this context menu michael@0: let title = this._findTitle(target); michael@0: michael@0: for (let context in this.menus) { michael@0: let menu = this.menus[context]; michael@0: menu.sort((a,b) => { michael@0: if (a.order === b.order) { michael@0: return 0; michael@0: } michael@0: return (a.order > b.order) ? 1 : -1; michael@0: }); michael@0: } michael@0: michael@0: let useTabs = Object.keys(this.menus).length > 1; michael@0: let prompt = new Prompt({ michael@0: window: target.ownerDocument.defaultView, michael@0: title: useTabs ? undefined : title michael@0: }); michael@0: michael@0: let items = this._reformatList(target); michael@0: if (useTabs) { michael@0: prompt.addTabs({ michael@0: id: "tabs", michael@0: items: items michael@0: }); michael@0: } else { michael@0: prompt.setSingleChoiceItems(items); michael@0: } michael@0: michael@0: prompt.show(this._promptDone.bind(this, target, x, y, items)); michael@0: }, michael@0: michael@0: // Called when the contextmenu prompt is closed michael@0: _promptDone: function(target, x, y, items, data) { michael@0: if (data.button == -1) { michael@0: // Prompt was cancelled, or an ActionView was used. michael@0: return; michael@0: } michael@0: michael@0: let selectedItemId; michael@0: if (data.tabs) { michael@0: let menu = items[data.tabs.tab]; michael@0: selectedItemId = menu.items[data.tabs.item].id; michael@0: } else { michael@0: selectedItemId = items[data.list[0]].id michael@0: } michael@0: michael@0: let selectedItem = this._findMenuItem(selectedItemId); michael@0: this.menus = null; michael@0: michael@0: if (!selectedItem || !selectedItem.matches || !selectedItem.callback) { michael@0: return; michael@0: } michael@0: michael@0: // for menuitems added using the native UI, pass the dom element that matched that item to the callback michael@0: while (target) { michael@0: if (selectedItem.matches(target, x, y)) { michael@0: selectedItem.callback(target, x, y); michael@0: break; michael@0: } michael@0: target = target.parentNode; michael@0: } michael@0: }, michael@0: michael@0: // Called when the contextmenu is done propagating to content. If the event wasn't cancelled, will show a contextmenu. michael@0: handleEvent: function(aEvent) { michael@0: BrowserEventHandler._cancelTapHighlight(); michael@0: aEvent.target.ownerDocument.defaultView.removeEventListener("contextmenu", this, false); michael@0: this._show(aEvent); michael@0: }, michael@0: michael@0: // Called when a long press is observed in the native Java frontend. Will start the process of generating/showing a contextmenu. michael@0: observe: function(aSubject, aTopic, aData) { michael@0: let data = JSON.parse(aData); michael@0: // content gets first crack at cancelling context menus michael@0: this._sendToContent(data.x, data.y); michael@0: }, michael@0: michael@0: // XXX - These are stolen from Util.js, we should remove them if we bring it back michael@0: makeURLAbsolute: function makeURLAbsolute(base, url) { michael@0: // Note: makeURI() will throw if url is not a valid URI michael@0: return this.makeURI(url, null, this.makeURI(base)).spec; michael@0: }, michael@0: michael@0: makeURI: function makeURI(aURL, aOriginCharset, aBaseURI) { michael@0: return Services.io.newURI(aURL, aOriginCharset, aBaseURI); michael@0: }, michael@0: michael@0: _getLink: function(aElement) { michael@0: if (aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE && michael@0: ((aElement instanceof Ci.nsIDOMHTMLAnchorElement && aElement.href) || michael@0: (aElement instanceof Ci.nsIDOMHTMLAreaElement && aElement.href) || michael@0: aElement instanceof Ci.nsIDOMHTMLLinkElement || michael@0: aElement.getAttributeNS(kXLinkNamespace, "type") == "simple")) { michael@0: try { michael@0: let url = this._getLinkURL(aElement); michael@0: return Services.io.newURI(url, null, null); michael@0: } catch (e) {} michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: _disableInGuest: function _disableInGuest(selector) { michael@0: return { michael@0: matches: function _disableInGuestMatches(aElement, aX, aY) { michael@0: if (BrowserApp.isGuest) michael@0: return false; michael@0: return selector.matches(aElement, aX, aY); michael@0: } michael@0: }; michael@0: }, michael@0: michael@0: _getLinkURL: function ch_getLinkURL(aLink) { michael@0: let href = aLink.href; michael@0: if (href) michael@0: return href; michael@0: michael@0: href = aLink.getAttributeNS(kXLinkNamespace, "href"); michael@0: if (!href || !href.match(/\S/)) { michael@0: // Without this we try to save as the current doc, michael@0: // for example, HTML case also throws if empty michael@0: throw "Empty href"; michael@0: } michael@0: michael@0: return this.makeURLAbsolute(aLink.baseURI, href); michael@0: }, michael@0: michael@0: _copyStringToDefaultClipboard: function(aString) { michael@0: let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper); michael@0: clipboard.copyString(aString); michael@0: }, michael@0: michael@0: _shareStringWithDefault: function(aSharedString, aTitle) { michael@0: let sharing = Cc["@mozilla.org/uriloader/external-sharing-app-service;1"].getService(Ci.nsIExternalSharingAppService); michael@0: sharing.shareWithDefault(aSharedString, "text/plain", aTitle); michael@0: }, michael@0: michael@0: _stripScheme: function(aString) { michael@0: let index = aString.indexOf(":"); michael@0: return aString.slice(index + 1); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: var LightWeightThemeWebInstaller = { michael@0: init: function sh_init() { michael@0: let temp = {}; michael@0: Cu.import("resource://gre/modules/LightweightThemeConsumer.jsm", temp); michael@0: let theme = new temp.LightweightThemeConsumer(document); michael@0: BrowserApp.deck.addEventListener("InstallBrowserTheme", this, false, true); michael@0: BrowserApp.deck.addEventListener("PreviewBrowserTheme", this, false, true); michael@0: BrowserApp.deck.addEventListener("ResetBrowserThemePreview", this, false, true); michael@0: }, michael@0: michael@0: uninit: function() { michael@0: BrowserApp.deck.addEventListener("InstallBrowserTheme", this, false, true); michael@0: BrowserApp.deck.addEventListener("PreviewBrowserTheme", this, false, true); michael@0: BrowserApp.deck.addEventListener("ResetBrowserThemePreview", this, false, true); michael@0: }, michael@0: michael@0: handleEvent: function (event) { michael@0: switch (event.type) { michael@0: case "InstallBrowserTheme": michael@0: case "PreviewBrowserTheme": michael@0: case "ResetBrowserThemePreview": michael@0: // ignore requests from background tabs michael@0: if (event.target.ownerDocument.defaultView.top != content) michael@0: return; michael@0: } michael@0: michael@0: switch (event.type) { michael@0: case "InstallBrowserTheme": michael@0: this._installRequest(event); michael@0: break; michael@0: case "PreviewBrowserTheme": michael@0: this._preview(event); michael@0: break; michael@0: case "ResetBrowserThemePreview": michael@0: this._resetPreview(event); michael@0: break; michael@0: case "pagehide": michael@0: case "TabSelect": michael@0: this._resetPreview(); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: get _manager () { michael@0: let temp = {}; michael@0: Cu.import("resource://gre/modules/LightweightThemeManager.jsm", temp); michael@0: delete this._manager; michael@0: return this._manager = temp.LightweightThemeManager; michael@0: }, michael@0: michael@0: _installRequest: function (event) { michael@0: let node = event.target; michael@0: let data = this._getThemeFromNode(node); michael@0: if (!data) michael@0: return; michael@0: michael@0: if (this._isAllowed(node)) { michael@0: this._install(data); michael@0: return; michael@0: } michael@0: michael@0: let allowButtonText = Strings.browser.GetStringFromName("lwthemeInstallRequest.allowButton"); michael@0: let message = Strings.browser.formatStringFromName("lwthemeInstallRequest.message", [node.ownerDocument.location.hostname], 1); michael@0: let buttons = [{ michael@0: label: allowButtonText, michael@0: callback: function () { michael@0: LightWeightThemeWebInstaller._install(data); michael@0: } michael@0: }]; michael@0: michael@0: NativeWindow.doorhanger.show(message, "Personas", buttons, BrowserApp.selectedTab.id); michael@0: }, michael@0: michael@0: _install: function (newLWTheme) { michael@0: this._manager.currentTheme = newLWTheme; michael@0: }, michael@0: michael@0: _previewWindow: null, michael@0: _preview: function (event) { michael@0: if (!this._isAllowed(event.target)) michael@0: return; michael@0: let data = this._getThemeFromNode(event.target); michael@0: if (!data) michael@0: return; michael@0: this._resetPreview(); michael@0: michael@0: this._previewWindow = event.target.ownerDocument.defaultView; michael@0: this._previewWindow.addEventListener("pagehide", this, true); michael@0: BrowserApp.deck.addEventListener("TabSelect", this, false); michael@0: this._manager.previewTheme(data); michael@0: }, michael@0: michael@0: _resetPreview: function (event) { michael@0: if (!this._previewWindow || michael@0: event && !this._isAllowed(event.target)) michael@0: return; michael@0: michael@0: this._previewWindow.removeEventListener("pagehide", this, true); michael@0: this._previewWindow = null; michael@0: BrowserApp.deck.removeEventListener("TabSelect", this, false); michael@0: michael@0: this._manager.resetPreview(); michael@0: }, michael@0: michael@0: _isAllowed: function (node) { michael@0: let pm = Services.perms; michael@0: michael@0: let uri = node.ownerDocument.documentURIObject; michael@0: return pm.testPermission(uri, "install") == pm.ALLOW_ACTION; michael@0: }, michael@0: michael@0: _getThemeFromNode: function (node) { michael@0: return this._manager.parseTheme(node.getAttribute("data-browsertheme"), node.baseURI); michael@0: } michael@0: }; michael@0: michael@0: var DesktopUserAgent = { michael@0: DESKTOP_UA: null, michael@0: michael@0: init: function ua_init() { michael@0: Services.obs.addObserver(this, "DesktopMode:Change", false); michael@0: UserAgentOverrides.addComplexOverride(this.onRequest.bind(this)); michael@0: michael@0: // See https://developer.mozilla.org/en/Gecko_user_agent_string_reference michael@0: this.DESKTOP_UA = Cc["@mozilla.org/network/protocol;1?name=http"] michael@0: .getService(Ci.nsIHttpProtocolHandler).userAgent michael@0: .replace(/Android; [a-zA-Z]+/, "X11; Linux x86_64") michael@0: .replace(/Gecko\/[0-9\.]+/, "Gecko/20100101"); michael@0: }, michael@0: michael@0: uninit: function ua_uninit() { michael@0: Services.obs.removeObserver(this, "DesktopMode:Change"); michael@0: }, michael@0: michael@0: onRequest: function(channel, defaultUA) { michael@0: let channelWindow = this._getWindowForRequest(channel); michael@0: let tab = BrowserApp.getTabForWindow(channelWindow); michael@0: if (tab == null) michael@0: return null; michael@0: michael@0: return this.getUserAgentForTab(tab); michael@0: }, michael@0: michael@0: getUserAgentForWindow: function ua_getUserAgentForWindow(aWindow) { michael@0: let tab = BrowserApp.getTabForWindow(aWindow.top); michael@0: if (tab) michael@0: return this.getUserAgentForTab(tab); michael@0: michael@0: return null; michael@0: }, michael@0: michael@0: getUserAgentForTab: function ua_getUserAgentForTab(aTab) { michael@0: // Send desktop UA if "Request Desktop Site" is enabled. michael@0: if (aTab.desktopMode) michael@0: return this.DESKTOP_UA; michael@0: michael@0: return null; michael@0: }, michael@0: michael@0: _getRequestLoadContext: function ua_getRequestLoadContext(aRequest) { michael@0: if (aRequest && aRequest.notificationCallbacks) { michael@0: try { michael@0: return aRequest.notificationCallbacks.getInterface(Ci.nsILoadContext); michael@0: } catch (ex) { } michael@0: } michael@0: michael@0: if (aRequest && aRequest.loadGroup && aRequest.loadGroup.notificationCallbacks) { michael@0: try { michael@0: return aRequest.loadGroup.notificationCallbacks.getInterface(Ci.nsILoadContext); michael@0: } catch (ex) { } michael@0: } michael@0: michael@0: return null; michael@0: }, michael@0: michael@0: _getWindowForRequest: function ua_getWindowForRequest(aRequest) { michael@0: let loadContext = this._getRequestLoadContext(aRequest); michael@0: if (loadContext) { michael@0: try { michael@0: return loadContext.associatedWindow; michael@0: } catch (e) { michael@0: // loadContext.associatedWindow can throw when there's no window michael@0: } michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: observe: function ua_observe(aSubject, aTopic, aData) { michael@0: if (aTopic === "DesktopMode:Change") { michael@0: let args = JSON.parse(aData); michael@0: let tab = BrowserApp.getTabForId(args.tabId); michael@0: if (tab != null) michael@0: tab.reloadWithMode(args.desktopMode); michael@0: } michael@0: } michael@0: }; michael@0: michael@0: michael@0: function nsBrowserAccess() { michael@0: } michael@0: michael@0: nsBrowserAccess.prototype = { michael@0: QueryInterface: XPCOMUtils.generateQI([Ci.nsIBrowserDOMWindow]), michael@0: michael@0: _getBrowser: function _getBrowser(aURI, aOpener, aWhere, aContext) { michael@0: let isExternal = (aContext == Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL); michael@0: if (isExternal && aURI && aURI.schemeIs("chrome")) michael@0: return null; michael@0: michael@0: let loadflags = isExternal ? michael@0: Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL : michael@0: Ci.nsIWebNavigation.LOAD_FLAGS_NONE; michael@0: if (aWhere == Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW) { michael@0: switch (aContext) { michael@0: case Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL: michael@0: aWhere = Services.prefs.getIntPref("browser.link.open_external"); michael@0: break; michael@0: default: // OPEN_NEW or an illegal value michael@0: aWhere = Services.prefs.getIntPref("browser.link.open_newwindow"); michael@0: } michael@0: } michael@0: michael@0: Services.io.offline = false; michael@0: michael@0: let referrer; michael@0: if (aOpener) { michael@0: try { michael@0: let location = aOpener.location; michael@0: referrer = Services.io.newURI(location, null, null); michael@0: } catch(e) { } michael@0: } michael@0: michael@0: let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore); michael@0: let pinned = false; michael@0: michael@0: if (aURI && aWhere == Ci.nsIBrowserDOMWindow.OPEN_SWITCHTAB) { michael@0: pinned = true; michael@0: let spec = aURI.spec; michael@0: let tabs = BrowserApp.tabs; michael@0: for (let i = 0; i < tabs.length; i++) { michael@0: let appOrigin = ss.getTabValue(tabs[i], "appOrigin"); michael@0: if (appOrigin == spec) { michael@0: let tab = tabs[i]; michael@0: BrowserApp.selectTab(tab); michael@0: return tab.browser; michael@0: } michael@0: } michael@0: } michael@0: michael@0: let newTab = (aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW || michael@0: aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWTAB || michael@0: aWhere == Ci.nsIBrowserDOMWindow.OPEN_SWITCHTAB); michael@0: let isPrivate = false; michael@0: michael@0: if (newTab) { michael@0: let parentId = -1; michael@0: if (!isExternal && aOpener) { michael@0: let parent = BrowserApp.getTabForWindow(aOpener.top); michael@0: if (parent) { michael@0: parentId = parent.id; michael@0: isPrivate = PrivateBrowsingUtils.isWindowPrivate(parent.browser.contentWindow); michael@0: } michael@0: } michael@0: michael@0: // BrowserApp.addTab calls loadURIWithFlags with the appropriate params michael@0: let tab = BrowserApp.addTab(aURI ? aURI.spec : "about:blank", { flags: loadflags, michael@0: referrerURI: referrer, michael@0: external: isExternal, michael@0: parentId: parentId, michael@0: selected: true, michael@0: isPrivate: isPrivate, michael@0: pinned: pinned }); michael@0: michael@0: return tab.browser; michael@0: } michael@0: michael@0: // OPEN_CURRENTWINDOW and illegal values michael@0: let browser = BrowserApp.selectedBrowser; michael@0: if (aURI && browser) michael@0: browser.loadURIWithFlags(aURI.spec, loadflags, referrer, null, null); michael@0: michael@0: return browser; michael@0: }, michael@0: michael@0: openURI: function browser_openURI(aURI, aOpener, aWhere, aContext) { michael@0: let browser = this._getBrowser(aURI, aOpener, aWhere, aContext); michael@0: return browser ? browser.contentWindow : null; michael@0: }, michael@0: michael@0: openURIInFrame: function browser_openURIInFrame(aURI, aOpener, aWhere, aContext) { michael@0: let browser = this._getBrowser(aURI, aOpener, aWhere, aContext); michael@0: return browser ? browser.QueryInterface(Ci.nsIFrameLoaderOwner) : null; michael@0: }, michael@0: michael@0: isTabContentWindow: function(aWindow) { michael@0: return BrowserApp.getBrowserForWindow(aWindow) != null; michael@0: }, michael@0: michael@0: get contentWindow() { michael@0: return BrowserApp.selectedBrowser.contentWindow; michael@0: } michael@0: }; michael@0: michael@0: michael@0: // track the last known screen size so that new tabs michael@0: // get created with the right size rather than being 1x1 michael@0: let gScreenWidth = 1; michael@0: let gScreenHeight = 1; michael@0: let gReflowPending = null; michael@0: michael@0: // The margins that should be applied to the viewport for fixed position michael@0: // children. This is used to avoid browser chrome permanently obscuring michael@0: // fixed position content, and also to make sure window-sized pages take michael@0: // into account said browser chrome. michael@0: let gViewportMargins = { top: 0, right: 0, bottom: 0, left: 0}; michael@0: michael@0: function Tab(aURL, aParams) { michael@0: this.browser = null; michael@0: this.id = 0; michael@0: this.lastTouchedAt = Date.now(); michael@0: this._zoom = 1.0; michael@0: this._drawZoom = 1.0; michael@0: this._restoreZoom = false; michael@0: this._fixedMarginLeft = 0; michael@0: this._fixedMarginTop = 0; michael@0: this._fixedMarginRight = 0; michael@0: this._fixedMarginBottom = 0; michael@0: this._readerEnabled = false; michael@0: this._readerActive = false; michael@0: this.userScrollPos = { x: 0, y: 0 }; michael@0: this.viewportExcludesHorizontalMargins = true; michael@0: this.viewportExcludesVerticalMargins = true; michael@0: this.viewportMeasureCallback = null; michael@0: this.lastPageSizeAfterViewportRemeasure = { width: 0, height: 0 }; michael@0: this.contentDocumentIsDisplayed = true; michael@0: this.pluginDoorhangerTimeout = null; michael@0: this.shouldShowPluginDoorhanger = true; michael@0: this.clickToPlayPluginsActivated = false; michael@0: this.desktopMode = false; michael@0: this.originalURI = null; michael@0: this.savedArticle = null; michael@0: this.hasTouchListener = false; michael@0: this.browserWidth = 0; michael@0: this.browserHeight = 0; michael@0: michael@0: this.create(aURL, aParams); michael@0: } michael@0: michael@0: Tab.prototype = { michael@0: create: function(aURL, aParams) { michael@0: if (this.browser) michael@0: return; michael@0: michael@0: aParams = aParams || {}; michael@0: michael@0: this.browser = document.createElement("browser"); michael@0: this.browser.setAttribute("type", "content-targetable"); michael@0: this.setBrowserSize(kDefaultCSSViewportWidth, kDefaultCSSViewportHeight); michael@0: michael@0: // Make sure the previously selected panel remains selected. The selected panel of a deck is michael@0: // not stable when panels are added. michael@0: let selectedPanel = BrowserApp.deck.selectedPanel; michael@0: BrowserApp.deck.insertBefore(this.browser, aParams.sibling || null); michael@0: BrowserApp.deck.selectedPanel = selectedPanel; michael@0: michael@0: if (BrowserApp.manifestUrl) { michael@0: let appsService = Cc["@mozilla.org/AppsService;1"].getService(Ci.nsIAppsService); michael@0: let manifest = appsService.getAppByManifestURL(BrowserApp.manifestUrl); michael@0: if (manifest) { michael@0: let app = manifest.QueryInterface(Ci.mozIApplication); michael@0: this.browser.docShell.setIsApp(app.localId); michael@0: } michael@0: } michael@0: michael@0: // Must be called after appendChild so the docshell has been created. michael@0: this.setActive(false); michael@0: michael@0: let isPrivate = ("isPrivate" in aParams) && aParams.isPrivate; michael@0: if (isPrivate) { michael@0: this.browser.docShell.QueryInterface(Ci.nsILoadContext).usePrivateBrowsing = true; michael@0: } michael@0: michael@0: this.browser.stop(); michael@0: michael@0: let frameLoader = this.browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader; michael@0: frameLoader.renderMode = Ci.nsIFrameLoader.RENDER_MODE_ASYNC_SCROLL; michael@0: michael@0: // only set tab uri if uri is valid michael@0: let uri = null; michael@0: let title = aParams.title || aURL; michael@0: try { michael@0: uri = Services.io.newURI(aURL, null, null).spec; michael@0: } catch (e) {} michael@0: michael@0: // When the tab is stubbed from Java, there's a window between the stub michael@0: // creation and the tab creation in Gecko where the stub could be removed michael@0: // or the selected tab can change (which is easiest to hit during startup). michael@0: // To prevent these races, we need to differentiate between tab stubs from michael@0: // Java and new tabs from Gecko. michael@0: let stub = false; michael@0: michael@0: if (!aParams.zombifying) { michael@0: if ("tabID" in aParams) { michael@0: this.id = aParams.tabID; michael@0: stub = true; michael@0: } else { michael@0: let jni = new JNI(); michael@0: let cls = jni.findClass("org/mozilla/gecko/Tabs"); michael@0: let method = jni.getStaticMethodID(cls, "getNextTabId", "()I"); michael@0: this.id = jni.callStaticIntMethod(cls, method); michael@0: jni.close(); michael@0: } michael@0: michael@0: this.desktopMode = ("desktopMode" in aParams) ? aParams.desktopMode : false; michael@0: michael@0: let message = { michael@0: type: "Tab:Added", michael@0: tabID: this.id, michael@0: uri: uri, michael@0: parentId: ("parentId" in aParams) ? aParams.parentId : -1, michael@0: external: ("external" in aParams) ? aParams.external : false, michael@0: selected: ("selected" in aParams) ? aParams.selected : true, michael@0: title: title, michael@0: delayLoad: aParams.delayLoad || false, michael@0: desktopMode: this.desktopMode, michael@0: isPrivate: isPrivate, michael@0: stub: stub michael@0: }; michael@0: sendMessageToJava(message); michael@0: michael@0: this.overscrollController = new OverscrollController(this); michael@0: } michael@0: michael@0: this.browser.contentWindow.controllers.insertControllerAt(0, this.overscrollController); michael@0: michael@0: let flags = Ci.nsIWebProgress.NOTIFY_STATE_ALL | michael@0: Ci.nsIWebProgress.NOTIFY_LOCATION | michael@0: Ci.nsIWebProgress.NOTIFY_SECURITY; michael@0: this.browser.addProgressListener(this, flags); michael@0: this.browser.sessionHistory.addSHistoryListener(this); michael@0: michael@0: this.browser.addEventListener("DOMContentLoaded", this, true); michael@0: this.browser.addEventListener("DOMFormHasPassword", this, true); michael@0: this.browser.addEventListener("DOMLinkAdded", this, true); michael@0: this.browser.addEventListener("DOMTitleChanged", this, true); michael@0: this.browser.addEventListener("DOMWindowClose", this, true); michael@0: this.browser.addEventListener("DOMWillOpenModalDialog", this, true); michael@0: this.browser.addEventListener("DOMAutoComplete", this, true); michael@0: this.browser.addEventListener("blur", this, true); michael@0: this.browser.addEventListener("scroll", this, true); michael@0: this.browser.addEventListener("MozScrolledAreaChanged", this, true); michael@0: this.browser.addEventListener("pageshow", this, true); michael@0: this.browser.addEventListener("MozApplicationManifest", this, true); michael@0: michael@0: // Note that the XBL binding is untrusted michael@0: this.browser.addEventListener("PluginBindingAttached", this, true, true); michael@0: this.browser.addEventListener("VideoBindingAttached", this, true, true); michael@0: this.browser.addEventListener("VideoBindingCast", this, true, true); michael@0: michael@0: Services.obs.addObserver(this, "before-first-paint", false); michael@0: Services.obs.addObserver(this, "after-viewport-change", false); michael@0: Services.prefs.addObserver("browser.ui.zoom.force-user-scalable", this, false); michael@0: michael@0: if (aParams.delayLoad) { michael@0: // If this is a zombie tab, attach restore data so the tab will be michael@0: // restored when selected michael@0: this.browser.__SS_data = { michael@0: entries: [{ michael@0: url: aURL, michael@0: title: title michael@0: }], michael@0: index: 1 michael@0: }; michael@0: this.browser.__SS_restore = true; michael@0: } else { michael@0: let flags = "flags" in aParams ? aParams.flags : Ci.nsIWebNavigation.LOAD_FLAGS_NONE; michael@0: let postData = ("postData" in aParams && aParams.postData) ? aParams.postData.value : null; michael@0: let referrerURI = "referrerURI" in aParams ? aParams.referrerURI : null; michael@0: let charset = "charset" in aParams ? aParams.charset : null; michael@0: michael@0: // The search term the user entered to load the current URL michael@0: this.userSearch = "userSearch" in aParams ? aParams.userSearch : ""; michael@0: michael@0: try { michael@0: this.browser.loadURIWithFlags(aURL, flags, referrerURI, charset, postData); michael@0: } catch(e) { michael@0: let message = { michael@0: type: "Content:LoadError", michael@0: tabID: this.id michael@0: }; michael@0: sendMessageToJava(message); michael@0: dump("Handled load error: " + e); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Retrieves the font size in twips for a given element. michael@0: */ michael@0: getInflatedFontSizeFor: function(aElement) { michael@0: // GetComputedStyle should always give us CSS pixels for a font size. michael@0: let fontSizeStr = this.window.getComputedStyle(aElement)['fontSize']; michael@0: let fontSize = fontSizeStr.slice(0, -2); michael@0: return aElement.fontSizeInflation * fontSize; michael@0: }, michael@0: michael@0: /** michael@0: * This returns the zoom necessary to match the font size of an element to michael@0: * the minimum font size specified by the browser.zoom.reflowOnZoom.minFontSizeTwips michael@0: * preference. michael@0: */ michael@0: getZoomToMinFontSize: function(aElement) { michael@0: // We only use the font.size.inflation.minTwips preference because this is michael@0: // the only one that is controlled by the user-interface in the 'Settings' michael@0: // menu. Thus, if font.size.inflation.emPerLine is changed, this does not michael@0: // effect reflow-on-zoom. michael@0: let minFontSize = convertFromTwipsToPx(Services.prefs.getIntPref("font.size.inflation.minTwips")); michael@0: return minFontSize / this.getInflatedFontSizeFor(aElement); michael@0: }, michael@0: michael@0: clearReflowOnZoomPendingActions: function() { michael@0: // Reflow was completed, so now re-enable painting. michael@0: let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation); michael@0: let docShell = webNav.QueryInterface(Ci.nsIDocShell); michael@0: let docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer); michael@0: docViewer.resumePainting(); michael@0: michael@0: BrowserApp.selectedTab._mReflozPositioned = false; michael@0: }, michael@0: michael@0: /** michael@0: * Reflow on zoom consists of a few different sub-operations: michael@0: * michael@0: * 1. When a double-tap event is seen, we verify that the correct preferences michael@0: * are enabled and perform the pre-position handling calculation. We also michael@0: * signal that reflow-on-zoom should be performed at this time, and pause michael@0: * painting. michael@0: * 2. During the next call to setViewport(), which is in the Tab prototype, michael@0: * we detect that a call to changeMaxLineBoxWidth should be performed. If michael@0: * we're zooming out, then the max line box width should be reset at this michael@0: * time. Otherwise, we call performReflowOnZoom. michael@0: * 2a. PerformReflowOnZoom() and resetMaxLineBoxWidth() schedule a call to michael@0: * doChangeMaxLineBoxWidth, based on a timeout specified in preferences. michael@0: * 3. doChangeMaxLineBoxWidth changes the line box width (which also michael@0: * schedules a reflow event), and then calls ZoomHelper.zoomInAndSnapToRange. michael@0: * 4. ZoomHelper.zoomInAndSnapToRange performs the positioning of reflow-on-zoom michael@0: * and then re-enables painting. michael@0: * michael@0: * Some of the events happen synchronously, while others happen asynchronously. michael@0: * The following is a rough sketch of the progression of events: michael@0: * michael@0: * double tap event seen -> onDoubleTap() -> ... asynchronous ... michael@0: * -> setViewport() -> performReflowOnZoom() -> ... asynchronous ... michael@0: * -> doChangeMaxLineBoxWidth() -> ZoomHelper.zoomInAndSnapToRange() michael@0: * -> ... asynchronous ... -> setViewport() -> Observe('after-viewport-change') michael@0: * -> resumePainting() michael@0: */ michael@0: performReflowOnZoom: function(aViewport) { michael@0: let zoom = this._drawZoom ? this._drawZoom : aViewport.zoom; michael@0: michael@0: let viewportWidth = gScreenWidth / zoom; michael@0: let reflozTimeout = Services.prefs.getIntPref("browser.zoom.reflowZoom.reflowTimeout"); michael@0: michael@0: if (gReflowPending) { michael@0: clearTimeout(gReflowPending); michael@0: } michael@0: michael@0: // We add in a bit of fudge just so that the end characters michael@0: // don't accidentally get clipped. 15px is an arbitrary choice. michael@0: gReflowPending = setTimeout(doChangeMaxLineBoxWidth, michael@0: reflozTimeout, michael@0: viewportWidth - 15); michael@0: }, michael@0: michael@0: /** michael@0: * Reloads the tab with the desktop mode setting. michael@0: */ michael@0: reloadWithMode: function (aDesktopMode) { michael@0: // Set desktop mode for tab and send change to Java michael@0: if (this.desktopMode != aDesktopMode) { michael@0: this.desktopMode = aDesktopMode; michael@0: sendMessageToJava({ michael@0: type: "DesktopMode:Changed", michael@0: desktopMode: aDesktopMode, michael@0: tabID: this.id michael@0: }); michael@0: } michael@0: michael@0: // Only reload the page for http/https schemes michael@0: let currentURI = this.browser.currentURI; michael@0: if (!currentURI.schemeIs("http") && !currentURI.schemeIs("https")) michael@0: return; michael@0: michael@0: let url = currentURI.spec; michael@0: let flags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE | michael@0: Ci.nsIWebNavigation.LOAD_FLAGS_REPLACE_HISTORY; michael@0: if (this.originalURI && !this.originalURI.equals(currentURI)) { michael@0: // We were redirected; reload the original URL michael@0: url = this.originalURI.spec; michael@0: } michael@0: michael@0: this.browser.docShell.loadURI(url, flags, null, null, null); michael@0: }, michael@0: michael@0: destroy: function() { michael@0: if (!this.browser) michael@0: return; michael@0: michael@0: this.browser.contentWindow.controllers.removeController(this.overscrollController); michael@0: michael@0: this.browser.removeProgressListener(this); michael@0: this.browser.sessionHistory.removeSHistoryListener(this); michael@0: michael@0: this.browser.removeEventListener("DOMContentLoaded", this, true); michael@0: this.browser.removeEventListener("DOMFormHasPassword", this, true); michael@0: this.browser.removeEventListener("DOMLinkAdded", this, true); michael@0: this.browser.removeEventListener("DOMTitleChanged", this, true); michael@0: this.browser.removeEventListener("DOMWindowClose", this, true); michael@0: this.browser.removeEventListener("DOMWillOpenModalDialog", this, true); michael@0: this.browser.removeEventListener("DOMAutoComplete", this, true); michael@0: this.browser.removeEventListener("blur", this, true); michael@0: this.browser.removeEventListener("scroll", this, true); michael@0: this.browser.removeEventListener("MozScrolledAreaChanged", this, true); michael@0: this.browser.removeEventListener("pageshow", this, true); michael@0: this.browser.removeEventListener("MozApplicationManifest", this, true); michael@0: michael@0: this.browser.removeEventListener("PluginBindingAttached", this, true, true); michael@0: this.browser.removeEventListener("VideoBindingAttached", this, true, true); michael@0: this.browser.removeEventListener("VideoBindingCast", this, true, true); michael@0: michael@0: Services.obs.removeObserver(this, "before-first-paint"); michael@0: Services.obs.removeObserver(this, "after-viewport-change"); michael@0: Services.prefs.removeObserver("browser.ui.zoom.force-user-scalable", this); michael@0: michael@0: // Make sure the previously selected panel remains selected. The selected panel of a deck is michael@0: // not stable when panels are removed. michael@0: let selectedPanel = BrowserApp.deck.selectedPanel; michael@0: BrowserApp.deck.removeChild(this.browser); michael@0: BrowserApp.deck.selectedPanel = selectedPanel; michael@0: michael@0: this.browser = null; michael@0: this.savedArticle = null; michael@0: }, michael@0: michael@0: // This should be called to update the browser when the tab gets selected/unselected michael@0: setActive: function setActive(aActive) { michael@0: if (!this.browser || !this.browser.docShell) michael@0: return; michael@0: michael@0: this.lastTouchedAt = Date.now(); michael@0: michael@0: if (aActive) { michael@0: this.browser.setAttribute("type", "content-primary"); michael@0: this.browser.focus(); michael@0: this.browser.docShellIsActive = true; michael@0: Reader.updatePageAction(this); michael@0: ExternalApps.updatePageAction(this.browser.currentURI); michael@0: } else { michael@0: this.browser.setAttribute("type", "content-targetable"); michael@0: this.browser.docShellIsActive = false; michael@0: } michael@0: }, michael@0: michael@0: getActive: function getActive() { michael@0: return this.browser.docShellIsActive; michael@0: }, michael@0: michael@0: setDisplayPort: function(aDisplayPort) { michael@0: let zoom = this._zoom; michael@0: let resolution = aDisplayPort.resolution; michael@0: if (zoom <= 0 || resolution <= 0) michael@0: return; michael@0: michael@0: // "zoom" is the user-visible zoom of the "this" tab michael@0: // "resolution" is the zoom at which we wish gecko to render "this" tab at michael@0: // these two may be different if we are, for example, trying to render a michael@0: // large area of the page at low resolution because the user is panning real michael@0: // fast. michael@0: // The gecko scroll position is in CSS pixels. The display port rect michael@0: // values (aDisplayPort), however, are in CSS pixels multiplied by the desired michael@0: // rendering resolution. Therefore care must be taken when doing math with michael@0: // these sets of values, to ensure that they are normalized to the same coordinate michael@0: // space first. michael@0: michael@0: let element = this.browser.contentDocument.documentElement; michael@0: if (!element) michael@0: return; michael@0: michael@0: // we should never be drawing background tabs at resolutions other than the user- michael@0: // visible zoom. for foreground tabs, however, if we are drawing at some other michael@0: // resolution, we need to set the resolution as specified. michael@0: let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); michael@0: if (BrowserApp.selectedTab == this) { michael@0: if (resolution != this._drawZoom) { michael@0: this._drawZoom = resolution; michael@0: cwu.setResolution(resolution / window.devicePixelRatio, resolution / window.devicePixelRatio); michael@0: } michael@0: } else if (!fuzzyEquals(resolution, zoom)) { michael@0: dump("Warning: setDisplayPort resolution did not match zoom for background tab! (" + resolution + " != " + zoom + ")"); michael@0: } michael@0: michael@0: // Finally, we set the display port, taking care to convert everything into the CSS-pixel michael@0: // coordinate space, because that is what the function accepts. Also we have to fudge the michael@0: // displayport somewhat to make sure it gets through all the conversions gecko will do on it michael@0: // without deforming too much. See https://bugzilla.mozilla.org/show_bug.cgi?id=737510#c10 michael@0: // for details on what these operations are. michael@0: let geckoScrollX = this.browser.contentWindow.scrollX; michael@0: let geckoScrollY = this.browser.contentWindow.scrollY; michael@0: aDisplayPort = this._dirtiestHackEverToWorkAroundGeckoRounding(aDisplayPort, geckoScrollX, geckoScrollY); michael@0: michael@0: let displayPort = { michael@0: x: (aDisplayPort.left / resolution) - geckoScrollX, michael@0: y: (aDisplayPort.top / resolution) - geckoScrollY, michael@0: width: (aDisplayPort.right - aDisplayPort.left) / resolution, michael@0: height: (aDisplayPort.bottom - aDisplayPort.top) / resolution michael@0: }; michael@0: michael@0: if (this._oldDisplayPort == null || michael@0: !fuzzyEquals(displayPort.x, this._oldDisplayPort.x) || michael@0: !fuzzyEquals(displayPort.y, this._oldDisplayPort.y) || michael@0: !fuzzyEquals(displayPort.width, this._oldDisplayPort.width) || michael@0: !fuzzyEquals(displayPort.height, this._oldDisplayPort.height)) { michael@0: if (BrowserApp.gUseLowPrecision) { michael@0: // Set the display-port to be 4x the size of the critical display-port, michael@0: // on each dimension, giving us a 0.25x lower precision buffer around the michael@0: // critical display-port. Spare area is *not* redistributed to the other michael@0: // axis, as display-list building and invalidation cost scales with the michael@0: // size of the display-port. michael@0: let pageRect = cwu.getRootBounds(); michael@0: let pageXMost = pageRect.right - geckoScrollX; michael@0: let pageYMost = pageRect.bottom - geckoScrollY; michael@0: michael@0: let dpW = Math.min(pageRect.right - pageRect.left, displayPort.width * 4); michael@0: let dpH = Math.min(pageRect.bottom - pageRect.top, displayPort.height * 4); michael@0: michael@0: let dpX = Math.min(Math.max(displayPort.x - displayPort.width * 1.5, michael@0: pageRect.left - geckoScrollX), pageXMost - dpW); michael@0: let dpY = Math.min(Math.max(displayPort.y - displayPort.height * 1.5, michael@0: pageRect.top - geckoScrollY), pageYMost - dpH); michael@0: cwu.setDisplayPortForElement(dpX, dpY, dpW, dpH, element, 0); michael@0: cwu.setCriticalDisplayPortForElement(displayPort.x, displayPort.y, michael@0: displayPort.width, displayPort.height, michael@0: element); michael@0: } else { michael@0: cwu.setDisplayPortForElement(displayPort.x, displayPort.y, michael@0: displayPort.width, displayPort.height, michael@0: element, 0); michael@0: } michael@0: } michael@0: michael@0: this._oldDisplayPort = displayPort; michael@0: }, michael@0: michael@0: /* michael@0: * Yes, this is ugly. But it's currently the safest way to account for the rounding errors that occur michael@0: * when we pump the displayport coordinates through gecko and they pop out in the compositor. michael@0: * michael@0: * In general, the values are converted from page-relative device pixels to viewport-relative app units, michael@0: * and then back to page-relative device pixels (now as ints). The first half of this is only slightly michael@0: * lossy, but it's enough to throw off the numbers a little. Because of this, when gecko calls michael@0: * ScaleToOutsidePixels to generate the final rect, the rect may get expanded more than it should, michael@0: * ending up a pixel larger than it started off. This is undesirable in general, but specifically michael@0: * bad for tiling, because it means we means we end up painting one line of pixels from a tile, michael@0: * causing an otherwise unnecessary upload of the whole tile. michael@0: * michael@0: * In order to counteract the rounding error, this code simulates the conversions that will happen michael@0: * to the display port, and calculates whether or not that final ScaleToOutsidePixels is actually michael@0: * expanding the rect more than it should. If so, it determines how much rounding error was introduced michael@0: * up until that point, and adjusts the original values to compensate for that rounding error. michael@0: */ michael@0: _dirtiestHackEverToWorkAroundGeckoRounding: function(aDisplayPort, aGeckoScrollX, aGeckoScrollY) { michael@0: const APP_UNITS_PER_CSS_PIXEL = 60.0; michael@0: const EXTRA_FUDGE = 0.04; michael@0: michael@0: let resolution = aDisplayPort.resolution; michael@0: michael@0: // Some helper functions that simulate conversion processes in gecko michael@0: michael@0: function cssPixelsToAppUnits(aVal) { michael@0: return Math.floor((aVal * APP_UNITS_PER_CSS_PIXEL) + 0.5); michael@0: } michael@0: michael@0: function appUnitsToDevicePixels(aVal) { michael@0: return aVal / APP_UNITS_PER_CSS_PIXEL * resolution; michael@0: } michael@0: michael@0: function devicePixelsToAppUnits(aVal) { michael@0: return cssPixelsToAppUnits(aVal / resolution); michael@0: } michael@0: michael@0: // Stash our original (desired) displayport width and height away, we need it michael@0: // later and we might modify the displayport in between. michael@0: let originalWidth = aDisplayPort.right - aDisplayPort.left; michael@0: let originalHeight = aDisplayPort.bottom - aDisplayPort.top; michael@0: michael@0: // This is the first conversion the displayport goes through, going from page-relative michael@0: // device pixels to viewport-relative app units. michael@0: let appUnitDisplayPort = { michael@0: x: cssPixelsToAppUnits((aDisplayPort.left / resolution) - aGeckoScrollX), michael@0: y: cssPixelsToAppUnits((aDisplayPort.top / resolution) - aGeckoScrollY), michael@0: w: cssPixelsToAppUnits((aDisplayPort.right - aDisplayPort.left) / resolution), michael@0: h: cssPixelsToAppUnits((aDisplayPort.bottom - aDisplayPort.top) / resolution) michael@0: }; michael@0: michael@0: // This is the translation gecko applies when converting back from viewport-relative michael@0: // device pixels to page-relative device pixels. michael@0: let geckoTransformX = -Math.floor((-aGeckoScrollX * resolution) + 0.5); michael@0: let geckoTransformY = -Math.floor((-aGeckoScrollY * resolution) + 0.5); michael@0: michael@0: // The final "left" value as calculated in gecko is: michael@0: // left = geckoTransformX + Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.x)) michael@0: // In a perfect world, this value would be identical to aDisplayPort.left, which is what michael@0: // we started with. However, this may not be the case if the value being floored has accumulated michael@0: // enough error to drop below what it should be. michael@0: // For example, assume geckoTransformX is 0, and aDisplayPort.left is 4, but michael@0: // appUnitsToDevicePixels(appUnitsToDevicePixels.x) comes out as 3.9 because of rounding error. michael@0: // That's bad, because the -0.1 error has caused it to floor to 3 instead of 4. (If it had errored michael@0: // the other way and come out as 4.1, there's no problem). In this example, we need to increase the michael@0: // "left" value by some amount so that the 3.9 actually comes out as >= 4, and it gets floored into michael@0: // the expected value of 4. The delta values calculated below calculate that error amount (e.g. -0.1). michael@0: let errorLeft = (geckoTransformX + appUnitsToDevicePixels(appUnitDisplayPort.x)) - aDisplayPort.left; michael@0: let errorTop = (geckoTransformY + appUnitsToDevicePixels(appUnitDisplayPort.y)) - aDisplayPort.top; michael@0: michael@0: // If the error was negative, that means it will floor incorrectly, so we need to bump up the michael@0: // original aDisplayPort.left and/or aDisplayPort.top values. The amount we bump it up by is michael@0: // the error amount (increased by a small fudge factor to ensure it's sufficient), converted michael@0: // backwards through the conversion process. michael@0: if (errorLeft < 0) { michael@0: aDisplayPort.left += appUnitsToDevicePixels(devicePixelsToAppUnits(EXTRA_FUDGE - errorLeft)); michael@0: // After we modify the left value, we need to re-simulate some values to take that into account michael@0: appUnitDisplayPort.x = cssPixelsToAppUnits((aDisplayPort.left / resolution) - aGeckoScrollX); michael@0: appUnitDisplayPort.w = cssPixelsToAppUnits((aDisplayPort.right - aDisplayPort.left) / resolution); michael@0: } michael@0: if (errorTop < 0) { michael@0: aDisplayPort.top += appUnitsToDevicePixels(devicePixelsToAppUnits(EXTRA_FUDGE - errorTop)); michael@0: // After we modify the top value, we need to re-simulate some values to take that into account michael@0: appUnitDisplayPort.y = cssPixelsToAppUnits((aDisplayPort.top / resolution) - aGeckoScrollY); michael@0: appUnitDisplayPort.h = cssPixelsToAppUnits((aDisplayPort.bottom - aDisplayPort.top) / resolution); michael@0: } michael@0: michael@0: // At this point, the aDisplayPort.left and aDisplayPort.top values have been corrected to account michael@0: // for the error in conversion such that they end up where we want them. Now we need to also do the michael@0: // same for the right/bottom values so that the width/height end up where we want them. michael@0: michael@0: // This is the final conversion that the displayport goes through before gecko spits it back to michael@0: // us. Note that the width/height calculates are of the form "ceil(transform(right)) - floor(transform(left))" michael@0: let scaledOutDevicePixels = { michael@0: x: Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.x)), michael@0: y: Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.y)), michael@0: w: Math.ceil(appUnitsToDevicePixels(appUnitDisplayPort.x + appUnitDisplayPort.w)) - Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.x)), michael@0: h: Math.ceil(appUnitsToDevicePixels(appUnitDisplayPort.y + appUnitDisplayPort.h)) - Math.floor(appUnitsToDevicePixels(appUnitDisplayPort.y)) michael@0: }; michael@0: michael@0: // The final "width" value as calculated in gecko is scaledOutDevicePixels.w. michael@0: // In a perfect world, this would equal originalWidth. However, things are not perfect, and as before, michael@0: // we need to calculate how much rounding error has been introduced. In this case the rounding error is causing michael@0: // the Math.ceil call above to ceiling to the wrong final value. For example, 4 gets converted 4.1 and gets michael@0: // ceiling'd to 5; in this case the error is 0.1. michael@0: let errorRight = (appUnitsToDevicePixels(appUnitDisplayPort.x + appUnitDisplayPort.w) - scaledOutDevicePixels.x) - originalWidth; michael@0: let errorBottom = (appUnitsToDevicePixels(appUnitDisplayPort.y + appUnitDisplayPort.h) - scaledOutDevicePixels.y) - originalHeight; michael@0: michael@0: // If the error was positive, that means it will ceiling incorrectly, so we need to bump down the michael@0: // original aDisplayPort.right and/or aDisplayPort.bottom. Again, we back-convert the error amount michael@0: // with a small fudge factor to figure out how much to adjust the original values. michael@0: if (errorRight > 0) aDisplayPort.right -= appUnitsToDevicePixels(devicePixelsToAppUnits(errorRight + EXTRA_FUDGE)); michael@0: if (errorBottom > 0) aDisplayPort.bottom -= appUnitsToDevicePixels(devicePixelsToAppUnits(errorBottom + EXTRA_FUDGE)); michael@0: michael@0: // Et voila! michael@0: return aDisplayPort; michael@0: }, michael@0: michael@0: setScrollClampingSize: function(zoom) { michael@0: let viewportWidth = gScreenWidth / zoom; michael@0: let viewportHeight = gScreenHeight / zoom; michael@0: let screenWidth = gScreenWidth; michael@0: let screenHeight = gScreenHeight; michael@0: michael@0: // Shrink the viewport appropriately if the margins are excluded michael@0: if (this.viewportExcludesVerticalMargins) { michael@0: screenHeight = gScreenHeight - gViewportMargins.top - gViewportMargins.bottom; michael@0: viewportHeight = screenHeight / zoom; michael@0: } michael@0: if (this.viewportExcludesHorizontalMargins) { michael@0: screenWidth = gScreenWidth - gViewportMargins.left - gViewportMargins.right; michael@0: viewportWidth = screenWidth / zoom; michael@0: } michael@0: michael@0: // Make sure the aspect ratio of the screen is maintained when setting michael@0: // the clamping scroll-port size. michael@0: let factor = Math.min(viewportWidth / screenWidth, michael@0: viewportHeight / screenHeight); michael@0: let scrollPortWidth = screenWidth * factor; michael@0: let scrollPortHeight = screenHeight * factor; michael@0: michael@0: let win = this.browser.contentWindow; michael@0: win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils). michael@0: setScrollPositionClampingScrollPortSize(scrollPortWidth, scrollPortHeight); michael@0: }, michael@0: michael@0: setViewport: function(aViewport) { michael@0: // Transform coordinates based on zoom michael@0: let x = aViewport.x / aViewport.zoom; michael@0: let y = aViewport.y / aViewport.zoom; michael@0: michael@0: this.setScrollClampingSize(aViewport.zoom); michael@0: michael@0: // Adjust the max line box width to be no more than the viewport width, but michael@0: // only if the reflow-on-zoom preference is enabled. michael@0: let isZooming = !fuzzyEquals(aViewport.zoom, this._zoom); michael@0: michael@0: let docViewer = null; michael@0: michael@0: if (isZooming && michael@0: BrowserEventHandler.mReflozPref && michael@0: BrowserApp.selectedTab._mReflozPoint && michael@0: BrowserApp.selectedTab.probablyNeedRefloz) { michael@0: let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation); michael@0: let docShell = webNav.QueryInterface(Ci.nsIDocShell); michael@0: docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer); michael@0: docViewer.pausePainting(); michael@0: michael@0: BrowserApp.selectedTab.performReflowOnZoom(aViewport); michael@0: BrowserApp.selectedTab.probablyNeedRefloz = false; michael@0: } michael@0: michael@0: let win = this.browser.contentWindow; michael@0: win.scrollTo(x, y); michael@0: this.saveSessionZoom(aViewport.zoom); michael@0: michael@0: this.userScrollPos.x = win.scrollX; michael@0: this.userScrollPos.y = win.scrollY; michael@0: this.setResolution(aViewport.zoom, false); michael@0: michael@0: if (aViewport.displayPort) michael@0: this.setDisplayPort(aViewport.displayPort); michael@0: michael@0: // Store fixed margins for later retrieval in getViewport. michael@0: this._fixedMarginLeft = aViewport.fixedMarginLeft; michael@0: this._fixedMarginTop = aViewport.fixedMarginTop; michael@0: this._fixedMarginRight = aViewport.fixedMarginRight; michael@0: this._fixedMarginBottom = aViewport.fixedMarginBottom; michael@0: michael@0: let dwi = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); michael@0: dwi.setContentDocumentFixedPositionMargins( michael@0: aViewport.fixedMarginTop / aViewport.zoom, michael@0: aViewport.fixedMarginRight / aViewport.zoom, michael@0: aViewport.fixedMarginBottom / aViewport.zoom, michael@0: aViewport.fixedMarginLeft / aViewport.zoom); michael@0: michael@0: Services.obs.notifyObservers(null, "after-viewport-change", ""); michael@0: if (docViewer) { michael@0: docViewer.resumePainting(); michael@0: } michael@0: }, michael@0: michael@0: setResolution: function(aZoom, aForce) { michael@0: // Set zoom level michael@0: if (aForce || !fuzzyEquals(aZoom, this._zoom)) { michael@0: this._zoom = aZoom; michael@0: if (BrowserApp.selectedTab == this) { michael@0: let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); michael@0: this._drawZoom = aZoom; michael@0: cwu.setResolution(aZoom / window.devicePixelRatio, aZoom / window.devicePixelRatio); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: getPageSize: function(aDocument, aDefaultWidth, aDefaultHeight) { michael@0: let body = aDocument.body || { scrollWidth: aDefaultWidth, scrollHeight: aDefaultHeight }; michael@0: let html = aDocument.documentElement || { scrollWidth: aDefaultWidth, scrollHeight: aDefaultHeight }; michael@0: return [Math.max(body.scrollWidth, html.scrollWidth), michael@0: Math.max(body.scrollHeight, html.scrollHeight)]; michael@0: }, michael@0: michael@0: getViewport: function() { michael@0: let screenW = gScreenWidth - gViewportMargins.left - gViewportMargins.right; michael@0: let screenH = gScreenHeight - gViewportMargins.top - gViewportMargins.bottom; michael@0: let zoom = this.restoredSessionZoom() || this._zoom; michael@0: michael@0: let viewport = { michael@0: width: screenW, michael@0: height: screenH, michael@0: cssWidth: screenW / zoom, michael@0: cssHeight: screenH / zoom, michael@0: pageLeft: 0, michael@0: pageTop: 0, michael@0: pageRight: screenW, michael@0: pageBottom: screenH, michael@0: // We make up matching css page dimensions michael@0: cssPageLeft: 0, michael@0: cssPageTop: 0, michael@0: cssPageRight: screenW / zoom, michael@0: cssPageBottom: screenH / zoom, michael@0: fixedMarginLeft: this._fixedMarginLeft, michael@0: fixedMarginTop: this._fixedMarginTop, michael@0: fixedMarginRight: this._fixedMarginRight, michael@0: fixedMarginBottom: this._fixedMarginBottom, michael@0: zoom: zoom, michael@0: }; michael@0: michael@0: // Set the viewport offset to current scroll offset michael@0: viewport.cssX = this.browser.contentWindow.scrollX || 0; michael@0: viewport.cssY = this.browser.contentWindow.scrollY || 0; michael@0: michael@0: // Transform coordinates based on zoom michael@0: viewport.x = Math.round(viewport.cssX * viewport.zoom); michael@0: viewport.y = Math.round(viewport.cssY * viewport.zoom); michael@0: michael@0: let doc = this.browser.contentDocument; michael@0: if (doc != null) { michael@0: let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); michael@0: let cssPageRect = cwu.getRootBounds(); michael@0: michael@0: /* michael@0: * Avoid sending page sizes of less than screen size before we hit DOMContentLoaded, because michael@0: * this causes the page size to jump around wildly during page load. After the page is loaded, michael@0: * send updates regardless of page size; we'll zoom to fit the content as needed. michael@0: * michael@0: * In the check below, we floor the viewport size because there might be slight rounding errors michael@0: * introduced in the CSS page size due to the conversion to and from app units in Gecko. The michael@0: * error should be no more than one app unit so doing the floor is overkill, but safe in the michael@0: * sense that the extra page size updates that get sent as a result will be mostly harmless. michael@0: */ michael@0: let pageLargerThanScreen = (cssPageRect.width >= Math.floor(viewport.cssWidth)) michael@0: && (cssPageRect.height >= Math.floor(viewport.cssHeight)); michael@0: if (doc.readyState === 'complete' || pageLargerThanScreen) { michael@0: viewport.cssPageLeft = cssPageRect.left; michael@0: viewport.cssPageTop = cssPageRect.top; michael@0: viewport.cssPageRight = cssPageRect.right; michael@0: viewport.cssPageBottom = cssPageRect.bottom; michael@0: /* Transform the page width and height based on the zoom factor. */ michael@0: viewport.pageLeft = (viewport.cssPageLeft * viewport.zoom); michael@0: viewport.pageTop = (viewport.cssPageTop * viewport.zoom); michael@0: viewport.pageRight = (viewport.cssPageRight * viewport.zoom); michael@0: viewport.pageBottom = (viewport.cssPageBottom * viewport.zoom); michael@0: } michael@0: } michael@0: michael@0: return viewport; michael@0: }, michael@0: michael@0: sendViewportUpdate: function(aPageSizeUpdate) { michael@0: let viewport = this.getViewport(); michael@0: let displayPort = Services.androidBridge.getDisplayPort(aPageSizeUpdate, BrowserApp.isBrowserContentDocumentDisplayed(), this.id, viewport); michael@0: if (displayPort != null) michael@0: this.setDisplayPort(displayPort); michael@0: }, michael@0: michael@0: updateViewportForPageSize: function() { michael@0: let hasHorizontalMargins = gViewportMargins.left != 0 || gViewportMargins.right != 0; michael@0: let hasVerticalMargins = gViewportMargins.top != 0 || gViewportMargins.bottom != 0; michael@0: michael@0: if (!hasHorizontalMargins && !hasVerticalMargins) { michael@0: // If there are no margins, then we don't need to do any remeasuring michael@0: return; michael@0: } michael@0: michael@0: // If the page size has changed so that it might or might not fit on the michael@0: // screen with the margins included, run updateViewportSize to resize the michael@0: // browser accordingly. michael@0: // A page will receive the smaller viewport when its page size fits michael@0: // within the screen size, so remeasure when the page size remains within michael@0: // the threshold of screen + margins, in case it's sizing itself relative michael@0: // to the viewport. michael@0: let viewport = this.getViewport(); michael@0: let pageWidth = viewport.pageRight - viewport.pageLeft; michael@0: let pageHeight = viewport.pageBottom - viewport.pageTop; michael@0: let remeasureNeeded = false; michael@0: michael@0: if (hasHorizontalMargins) { michael@0: let viewportShouldExcludeHorizontalMargins = (pageWidth <= gScreenWidth - 0.5); michael@0: if (viewportShouldExcludeHorizontalMargins != this.viewportExcludesHorizontalMargins) { michael@0: remeasureNeeded = true; michael@0: } michael@0: } michael@0: if (hasVerticalMargins) { michael@0: let viewportShouldExcludeVerticalMargins = (pageHeight <= gScreenHeight - 0.5); michael@0: if (viewportShouldExcludeVerticalMargins != this.viewportExcludesVerticalMargins) { michael@0: remeasureNeeded = true; michael@0: } michael@0: } michael@0: michael@0: if (remeasureNeeded) { michael@0: if (!this.viewportMeasureCallback) { michael@0: this.viewportMeasureCallback = setTimeout(function() { michael@0: this.viewportMeasureCallback = null; michael@0: michael@0: // Re-fetch the viewport as it may have changed between setting the timeout michael@0: // and running this callback michael@0: let viewport = this.getViewport(); michael@0: let pageWidth = viewport.pageRight - viewport.pageLeft; michael@0: let pageHeight = viewport.pageBottom - viewport.pageTop; michael@0: michael@0: if (Math.abs(pageWidth - this.lastPageSizeAfterViewportRemeasure.width) >= 0.5 || michael@0: Math.abs(pageHeight - this.lastPageSizeAfterViewportRemeasure.height) >= 0.5) { michael@0: this.updateViewportSize(gScreenWidth); michael@0: } michael@0: }.bind(this), kViewportRemeasureThrottle); michael@0: } michael@0: } else if (this.viewportMeasureCallback) { michael@0: // If the page changed size twice since we last measured the viewport and michael@0: // the latest size change reveals we don't need to remeasure, cancel any michael@0: // pending remeasure. michael@0: clearTimeout(this.viewportMeasureCallback); michael@0: this.viewportMeasureCallback = null; michael@0: } michael@0: }, michael@0: michael@0: handleEvent: function(aEvent) { michael@0: switch (aEvent.type) { michael@0: case "DOMContentLoaded": { michael@0: let target = aEvent.originalTarget; michael@0: michael@0: // ignore on frames and other documents michael@0: if (target != this.browser.contentDocument) michael@0: return; michael@0: michael@0: // Sample the background color of the page and pass it along. (This is used to draw the michael@0: // checkerboard.) Right now we don't detect changes in the background color after this michael@0: // event fires; it's not clear that doing so is worth the effort. michael@0: var backgroundColor = null; michael@0: try { michael@0: let { contentDocument, contentWindow } = this.browser; michael@0: let computedStyle = contentWindow.getComputedStyle(contentDocument.body); michael@0: backgroundColor = computedStyle.backgroundColor; michael@0: } catch (e) { michael@0: // Ignore. Catching and ignoring exceptions here ensures that Talos succeeds. michael@0: } michael@0: michael@0: let docURI = target.documentURI; michael@0: let errorType = ""; michael@0: if (docURI.startsWith("about:certerror")) michael@0: errorType = "certerror"; michael@0: else if (docURI.startsWith("about:blocked")) michael@0: errorType = "blocked" michael@0: else if (docURI.startsWith("about:neterror")) michael@0: errorType = "neterror"; michael@0: michael@0: sendMessageToJava({ michael@0: type: "DOMContentLoaded", michael@0: tabID: this.id, michael@0: bgColor: backgroundColor, michael@0: errorType: errorType michael@0: }); michael@0: michael@0: // Attach a listener to watch for "click" events bubbling up from error michael@0: // pages and other similar page. This lets us fix bugs like 401575 which michael@0: // require error page UI to do privileged things, without letting error michael@0: // pages have any privilege themselves. michael@0: if (docURI.startsWith("about:certerror") || docURI.startsWith("about:blocked")) { michael@0: this.browser.addEventListener("click", ErrorPageEventHandler, true); michael@0: let listener = function() { michael@0: this.browser.removeEventListener("click", ErrorPageEventHandler, true); michael@0: this.browser.removeEventListener("pagehide", listener, true); michael@0: }.bind(this); michael@0: michael@0: this.browser.addEventListener("pagehide", listener, true); michael@0: } michael@0: michael@0: if (docURI.startsWith("about:reader")) { michael@0: // During browser restart / recovery, duplicate "DOMContentLoaded" messages are received here michael@0: // For the visible tab ... where more than one tab is being reloaded, the inital "DOMContentLoaded" michael@0: // Message can be received before the document body is available ... so we avoid instantiating an michael@0: // AboutReader object, expecting that an eventual valid message will follow. michael@0: let contentDocument = this.browser.contentDocument; michael@0: if (contentDocument.body) { michael@0: new AboutReader(contentDocument, this.browser.contentWindow); michael@0: } michael@0: } michael@0: michael@0: break; michael@0: } michael@0: michael@0: case "DOMFormHasPassword": { michael@0: LoginManagerContent.onFormPassword(aEvent); michael@0: break; michael@0: } michael@0: michael@0: case "DOMLinkAdded": { michael@0: let target = aEvent.originalTarget; michael@0: if (!target.href || target.disabled) michael@0: return; michael@0: michael@0: // Ignore on frames and other documents michael@0: if (target.ownerDocument != this.browser.contentDocument) michael@0: return; michael@0: michael@0: // Sanitize the rel string michael@0: let list = []; michael@0: if (target.rel) { michael@0: list = target.rel.toLowerCase().split(/\s+/); michael@0: let hash = {}; michael@0: list.forEach(function(value) { hash[value] = true; }); michael@0: list = []; michael@0: for (let rel in hash) michael@0: list.push("[" + rel + "]"); michael@0: } michael@0: michael@0: if (list.indexOf("[icon]") != -1) { michael@0: // We want to get the largest icon size possible for our UI. michael@0: let maxSize = 0; michael@0: michael@0: // We use the sizes attribute if available michael@0: // see http://www.whatwg.org/specs/web-apps/current-work/multipage/links.html#rel-icon michael@0: if (target.hasAttribute("sizes")) { michael@0: let sizes = target.getAttribute("sizes").toLowerCase(); michael@0: michael@0: if (sizes == "any") { michael@0: // Since Java expects an integer, use -1 to represent icons with sizes="any" michael@0: maxSize = -1; michael@0: } else { michael@0: let tokens = sizes.split(" "); michael@0: tokens.forEach(function(token) { michael@0: // TODO: check for invalid tokens michael@0: let [w, h] = token.split("x"); michael@0: maxSize = Math.max(maxSize, Math.max(w, h)); michael@0: }); michael@0: } michael@0: } michael@0: michael@0: let json = { michael@0: type: "Link:Favicon", michael@0: tabID: this.id, michael@0: href: resolveGeckoURI(target.href), michael@0: charset: target.ownerDocument.characterSet, michael@0: title: target.title, michael@0: rel: list.join(" "), michael@0: size: maxSize michael@0: }; michael@0: sendMessageToJava(json); michael@0: } else if (list.indexOf("[alternate]") != -1) { michael@0: let type = target.type.toLowerCase().replace(/^\s+|\s*(?:;.*)?$/g, ""); michael@0: let isFeed = (type == "application/rss+xml" || type == "application/atom+xml"); michael@0: michael@0: if (!isFeed) michael@0: return; michael@0: michael@0: try { michael@0: // urlSecurityCeck will throw if things are not OK michael@0: ContentAreaUtils.urlSecurityCheck(target.href, target.ownerDocument.nodePrincipal, Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL); michael@0: michael@0: if (!this.browser.feeds) michael@0: this.browser.feeds = []; michael@0: this.browser.feeds.push({ href: target.href, title: target.title, type: type }); michael@0: michael@0: let json = { michael@0: type: "Link:Feed", michael@0: tabID: this.id michael@0: }; michael@0: sendMessageToJava(json); michael@0: } catch (e) {} michael@0: } else if (list.indexOf("[search]" != -1)) { michael@0: let type = target.type && target.type.toLowerCase(); michael@0: michael@0: // Replace all starting or trailing spaces or spaces before "*;" globally w/ "". michael@0: type = type.replace(/^\s+|\s*(?:;.*)?$/g, ""); michael@0: michael@0: // Check that type matches opensearch. michael@0: let isOpenSearch = (type == "application/opensearchdescription+xml"); michael@0: if (isOpenSearch && target.title && /^(?:https?|ftp):/i.test(target.href)) { michael@0: let visibleEngines = Services.search.getVisibleEngines(); michael@0: // NOTE: Engines are currently identified by name, but this can be changed michael@0: // when Engines are identified by URL (see bug 335102). michael@0: if (visibleEngines.some(function(e) { michael@0: return e.name == target.title; michael@0: })) { michael@0: // This engine is already present, do nothing. michael@0: return; michael@0: } michael@0: michael@0: if (this.browser.engines) { michael@0: // This engine has already been handled, do nothing. michael@0: if (this.browser.engines.some(function(e) { michael@0: return e.url == target.href; michael@0: })) { michael@0: return; michael@0: } michael@0: } else { michael@0: this.browser.engines = []; michael@0: } michael@0: michael@0: // Get favicon. michael@0: let iconURL = target.ownerDocument.documentURIObject.prePath + "/favicon.ico"; michael@0: michael@0: let newEngine = { michael@0: title: target.title, michael@0: url: target.href, michael@0: iconURL: iconURL michael@0: }; michael@0: michael@0: this.browser.engines.push(newEngine); michael@0: michael@0: // Don't send a message to display engines if we've already handled an engine. michael@0: if (this.browser.engines.length > 1) michael@0: return; michael@0: michael@0: // Broadcast message that this tab contains search engines that should be visible. michael@0: let newEngineMessage = { michael@0: type: "Link:OpenSearch", michael@0: tabID: this.id, michael@0: visible: true michael@0: }; michael@0: michael@0: sendMessageToJava(newEngineMessage); michael@0: } michael@0: } michael@0: break; michael@0: } michael@0: michael@0: case "DOMTitleChanged": { michael@0: if (!aEvent.isTrusted) michael@0: return; michael@0: michael@0: // ignore on frames and other documents michael@0: if (aEvent.originalTarget != this.browser.contentDocument) michael@0: return; michael@0: michael@0: sendMessageToJava({ michael@0: type: "DOMTitleChanged", michael@0: tabID: this.id, michael@0: title: aEvent.target.title.substring(0, 255) michael@0: }); michael@0: break; michael@0: } michael@0: michael@0: case "DOMWindowClose": { michael@0: if (!aEvent.isTrusted) michael@0: return; michael@0: michael@0: // Find the relevant tab, and close it from Java michael@0: if (this.browser.contentWindow == aEvent.target) { michael@0: aEvent.preventDefault(); michael@0: michael@0: sendMessageToJava({ michael@0: type: "Tab:Close", michael@0: tabID: this.id michael@0: }); michael@0: } michael@0: break; michael@0: } michael@0: michael@0: case "DOMWillOpenModalDialog": { michael@0: if (!aEvent.isTrusted) michael@0: return; michael@0: michael@0: // We're about to open a modal dialog, make sure the opening michael@0: // tab is brought to the front. michael@0: let tab = BrowserApp.getTabForWindow(aEvent.target.top); michael@0: BrowserApp.selectTab(tab); michael@0: break; michael@0: } michael@0: michael@0: case "DOMAutoComplete": michael@0: case "blur": { michael@0: LoginManagerContent.onUsernameInput(aEvent); michael@0: break; michael@0: } michael@0: michael@0: case "scroll": { michael@0: let win = this.browser.contentWindow; michael@0: if (this.userScrollPos.x != win.scrollX || this.userScrollPos.y != win.scrollY) { michael@0: this.sendViewportUpdate(); michael@0: } michael@0: break; michael@0: } michael@0: michael@0: case "MozScrolledAreaChanged": { michael@0: // This event is only fired for root scroll frames, and only when the michael@0: // scrolled area has actually changed, so no need to check for that. michael@0: // Just make sure it's the event for the correct root scroll frame. michael@0: if (aEvent.originalTarget != this.browser.contentDocument) michael@0: return; michael@0: michael@0: this.sendViewportUpdate(true); michael@0: this.updateViewportForPageSize(); michael@0: break; michael@0: } michael@0: michael@0: case "PluginBindingAttached": { michael@0: PluginHelper.handlePluginBindingAttached(this, aEvent); michael@0: break; michael@0: } michael@0: michael@0: case "VideoBindingAttached": { michael@0: CastingApps.handleVideoBindingAttached(this, aEvent); michael@0: break; michael@0: } michael@0: michael@0: case "VideoBindingCast": { michael@0: CastingApps.handleVideoBindingCast(this, aEvent); michael@0: break; michael@0: } michael@0: michael@0: case "MozApplicationManifest": { michael@0: OfflineApps.offlineAppRequested(aEvent.originalTarget.defaultView); michael@0: break; michael@0: } michael@0: michael@0: case "pageshow": { michael@0: // only send pageshow for the top-level document michael@0: if (aEvent.originalTarget.defaultView != this.browser.contentWindow) michael@0: return; michael@0: michael@0: sendMessageToJava({ michael@0: type: "Content:PageShow", michael@0: tabID: this.id michael@0: }); michael@0: michael@0: if (!aEvent.persisted && Services.prefs.getBoolPref("browser.ui.linkify.phone")) { michael@0: if (!this._linkifier) michael@0: this._linkifier = new Linkifier(); michael@0: this._linkifier.linkifyNumbers(this.browser.contentWindow.document); michael@0: } michael@0: michael@0: // Update page actions for helper apps. michael@0: let uri = this.browser.currentURI; michael@0: if (BrowserApp.selectedTab == this) { michael@0: if (ExternalApps.shouldCheckUri(uri)) { michael@0: ExternalApps.updatePageAction(uri); michael@0: } else { michael@0: ExternalApps.clearPageAction(); michael@0: } michael@0: } michael@0: michael@0: if (!Reader.isEnabledForParseOnLoad) michael@0: return; michael@0: michael@0: // Once document is fully loaded, parse it michael@0: Reader.parseDocumentFromTab(this.id, function (article) { michael@0: // Do nothing if there's no article or the page in this tab has michael@0: // changed michael@0: let tabURL = uri.specIgnoringRef; michael@0: if (article == null || (article.url != tabURL)) { michael@0: // Don't clear the article for about:reader pages since we want to michael@0: // use the article from the previous page michael@0: if (!tabURL.startsWith("about:reader")) { michael@0: this.savedArticle = null; michael@0: this.readerEnabled = false; michael@0: this.readerActive = false; michael@0: } else { michael@0: this.readerActive = true; michael@0: } michael@0: return; michael@0: } michael@0: michael@0: this.savedArticle = article; michael@0: michael@0: sendMessageToJava({ michael@0: type: "Content:ReaderEnabled", michael@0: tabID: this.id michael@0: }); michael@0: michael@0: if(this.readerActive) michael@0: this.readerActive = false; michael@0: michael@0: if(!this.readerEnabled) michael@0: this.readerEnabled = true; michael@0: }.bind(this)); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) { michael@0: let contentWin = aWebProgress.DOMWindow; michael@0: if (contentWin != contentWin.top) michael@0: return; michael@0: michael@0: // Filter optimization: Only really send NETWORK state changes to Java listener michael@0: if (aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) { michael@0: if ((aStateFlags & Ci.nsIWebProgressListener.STATE_STOP) && aWebProgress.isLoadingDocument) { michael@0: // We may receive a document stop event while a document is still loading michael@0: // (such as when doing URI fixup). Don't notify Java UI in these cases. michael@0: return; michael@0: } michael@0: michael@0: // Clear page-specific opensearch engines and feeds for a new request. michael@0: if (aStateFlags & Ci.nsIWebProgressListener.STATE_START && aRequest && aWebProgress.isTopLevel) { michael@0: this.browser.engines = null; michael@0: this.browser.feeds = null; michael@0: } michael@0: michael@0: // true if the page loaded successfully (i.e., no 404s or other errors) michael@0: let success = false; michael@0: let uri = ""; michael@0: try { michael@0: // Remember original URI for UA changes on redirected pages michael@0: this.originalURI = aRequest.QueryInterface(Components.interfaces.nsIChannel).originalURI; michael@0: michael@0: if (this.originalURI != null) michael@0: uri = this.originalURI.spec; michael@0: } catch (e) { } michael@0: try { michael@0: success = aRequest.QueryInterface(Components.interfaces.nsIHttpChannel).requestSucceeded; michael@0: } catch (e) { michael@0: // If the request does not handle the nsIHttpChannel interface, use nsIRequest's success michael@0: // status. Used for local files. See bug 948849. michael@0: success = aRequest.status == 0; michael@0: } michael@0: michael@0: // Check to see if we restoring the content from a previous presentation (session) michael@0: // since there should be no real network activity michael@0: let restoring = (aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) > 0; michael@0: michael@0: let message = { michael@0: type: "Content:StateChange", michael@0: tabID: this.id, michael@0: uri: uri, michael@0: state: aStateFlags, michael@0: restoring: restoring, michael@0: success: success michael@0: }; michael@0: sendMessageToJava(message); michael@0: } michael@0: }, michael@0: michael@0: onLocationChange: function(aWebProgress, aRequest, aLocationURI, aFlags) { michael@0: let contentWin = aWebProgress.DOMWindow; michael@0: michael@0: // Browser webapps may load content inside iframes that can not reach across the app/frame boundary michael@0: // i.e. even though the page is loaded in an iframe window.top != webapp michael@0: // Make cure this window is a top level tab before moving on. michael@0: if (BrowserApp.getBrowserForWindow(contentWin) == null) michael@0: return; michael@0: michael@0: this._hostChanged = true; michael@0: michael@0: let fixedURI = aLocationURI; michael@0: try { michael@0: fixedURI = URIFixup.createExposableURI(aLocationURI); michael@0: } catch (ex) { } michael@0: michael@0: let contentType = contentWin.document.contentType; michael@0: michael@0: // If fixedURI matches browser.lastURI, we assume this isn't a real location michael@0: // change but rather a spurious addition like a wyciwyg URI prefix. See Bug 747883. michael@0: // Note that we have to ensure fixedURI is not the same as aLocationURI so we michael@0: // don't false-positive page reloads as spurious additions. michael@0: let sameDocument = (aFlags & Ci.nsIWebProgressListener.LOCATION_CHANGE_SAME_DOCUMENT) != 0 || michael@0: ((this.browser.lastURI != null) && fixedURI.equals(this.browser.lastURI) && !fixedURI.equals(aLocationURI)); michael@0: this.browser.lastURI = fixedURI; michael@0: michael@0: // Reset state of click-to-play plugin notifications. michael@0: clearTimeout(this.pluginDoorhangerTimeout); michael@0: this.pluginDoorhangerTimeout = null; michael@0: this.shouldShowPluginDoorhanger = true; michael@0: this.clickToPlayPluginsActivated = false; michael@0: // Borrowed from desktop Firefox: http://mxr.mozilla.org/mozilla-central/source/browser/base/content/urlbarBindings.xml#174 michael@0: let documentURI = contentWin.document.documentURIObject.spec michael@0: let matchedURL = documentURI.match(/^((?:[a-z]+:\/\/)?(?:[^\/]+@)?)(.+?)(?::\d+)?(?:\/|$)/); michael@0: let baseDomain = ""; michael@0: if (matchedURL) { michael@0: var domain = ""; michael@0: [, , domain] = matchedURL; michael@0: michael@0: try { michael@0: baseDomain = Services.eTLD.getBaseDomainFromHost(domain); michael@0: if (!domain.endsWith(baseDomain)) { michael@0: // getBaseDomainFromHost converts its resultant to ACE. michael@0: let IDNService = Cc["@mozilla.org/network/idn-service;1"].getService(Ci.nsIIDNService); michael@0: baseDomain = IDNService.convertACEtoUTF8(baseDomain); michael@0: } michael@0: } catch (e) {} michael@0: } michael@0: michael@0: // Update the page actions URI for helper apps. michael@0: if (BrowserApp.selectedTab == this) { michael@0: ExternalApps.updatePageActionUri(fixedURI); michael@0: } michael@0: michael@0: let message = { michael@0: type: "Content:LocationChange", michael@0: tabID: this.id, michael@0: uri: fixedURI.spec, michael@0: userSearch: this.userSearch || "", michael@0: baseDomain: baseDomain, michael@0: contentType: (contentType ? contentType : ""), michael@0: sameDocument: sameDocument michael@0: }; michael@0: michael@0: sendMessageToJava(message); michael@0: michael@0: // The search term is only valid for this location change event, so reset it here. michael@0: this.userSearch = ""; michael@0: michael@0: if (!sameDocument) { michael@0: // XXX This code assumes that this is the earliest hook we have at which michael@0: // browser.contentDocument is changed to the new document we're loading michael@0: this.contentDocumentIsDisplayed = false; michael@0: this.hasTouchListener = false; michael@0: } else { michael@0: this.sendViewportUpdate(); michael@0: } michael@0: }, michael@0: michael@0: // Properties used to cache security state used to update the UI michael@0: _state: null, michael@0: _hostChanged: false, // onLocationChange will flip this bit michael@0: michael@0: onSecurityChange: function(aWebProgress, aRequest, aState) { michael@0: // Don't need to do anything if the data we use to update the UI hasn't changed michael@0: if (this._state == aState && !this._hostChanged) michael@0: return; michael@0: michael@0: this._state = aState; michael@0: this._hostChanged = false; michael@0: michael@0: let identity = IdentityHandler.checkIdentity(aState, this.browser); michael@0: michael@0: let message = { michael@0: type: "Content:SecurityChange", michael@0: tabID: this.id, michael@0: identity: identity michael@0: }; michael@0: michael@0: sendMessageToJava(message); michael@0: }, michael@0: michael@0: onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress) { michael@0: }, michael@0: michael@0: onStatusChange: function(aBrowser, aWebProgress, aRequest, aStatus, aMessage) { michael@0: }, michael@0: michael@0: _sendHistoryEvent: function(aMessage, aParams) { michael@0: let message = { michael@0: type: "SessionHistory:" + aMessage, michael@0: tabID: this.id, michael@0: }; michael@0: michael@0: // Restore zoom only when moving in session history, not for new page loads. michael@0: this._restoreZoom = aMessage != "New"; michael@0: michael@0: if (aParams) { michael@0: if ("url" in aParams) michael@0: message.url = aParams.url; michael@0: if ("index" in aParams) michael@0: message.index = aParams.index; michael@0: if ("numEntries" in aParams) michael@0: message.numEntries = aParams.numEntries; michael@0: } michael@0: michael@0: sendMessageToJava(message); michael@0: }, michael@0: michael@0: _getGeckoZoom: function() { michael@0: let res = {x: {}, y: {}}; michael@0: let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); michael@0: cwu.getResolution(res.x, res.y); michael@0: let zoom = res.x.value * window.devicePixelRatio; michael@0: return zoom; michael@0: }, michael@0: michael@0: saveSessionZoom: function(aZoom) { michael@0: let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); michael@0: cwu.setResolution(aZoom / window.devicePixelRatio, aZoom / window.devicePixelRatio); michael@0: }, michael@0: michael@0: restoredSessionZoom: function() { michael@0: let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); michael@0: michael@0: if (this._restoreZoom && cwu.isResolutionSet) { michael@0: return this._getGeckoZoom(); michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: OnHistoryNewEntry: function(aUri) { michael@0: this._sendHistoryEvent("New", { url: aUri.spec }); michael@0: }, michael@0: michael@0: OnHistoryGoBack: function(aUri) { michael@0: this._sendHistoryEvent("Back"); michael@0: return true; michael@0: }, michael@0: michael@0: OnHistoryGoForward: function(aUri) { michael@0: this._sendHistoryEvent("Forward"); michael@0: return true; michael@0: }, michael@0: michael@0: OnHistoryReload: function(aUri, aFlags) { michael@0: // we don't do anything with this, so don't propagate it michael@0: // for now anyway michael@0: return true; michael@0: }, michael@0: michael@0: OnHistoryGotoIndex: function(aIndex, aUri) { michael@0: this._sendHistoryEvent("Goto", { index: aIndex }); michael@0: return true; michael@0: }, michael@0: michael@0: OnHistoryPurge: function(aNumEntries) { michael@0: this._sendHistoryEvent("Purge", { numEntries: aNumEntries }); michael@0: return true; michael@0: }, michael@0: michael@0: OnHistoryReplaceEntry: function(aIndex) { michael@0: // we don't do anything with this, so don't propogate it michael@0: // for now anyway. michael@0: }, michael@0: michael@0: get metadata() { michael@0: return ViewportHandler.getMetadataForDocument(this.browser.contentDocument); michael@0: }, michael@0: michael@0: /** Update viewport when the metadata changes. */ michael@0: updateViewportMetadata: function updateViewportMetadata(aMetadata, aInitialLoad) { michael@0: if (Services.prefs.getBoolPref("browser.ui.zoom.force-user-scalable")) { michael@0: aMetadata.allowZoom = true; michael@0: aMetadata.allowDoubleTapZoom = true; michael@0: aMetadata.minZoom = aMetadata.maxZoom = NaN; michael@0: } michael@0: michael@0: let scaleRatio = window.devicePixelRatio; michael@0: michael@0: if (aMetadata.defaultZoom > 0) michael@0: aMetadata.defaultZoom *= scaleRatio; michael@0: if (aMetadata.minZoom > 0) michael@0: aMetadata.minZoom *= scaleRatio; michael@0: if (aMetadata.maxZoom > 0) michael@0: aMetadata.maxZoom *= scaleRatio; michael@0: michael@0: aMetadata.isRTL = this.browser.contentDocument.documentElement.dir == "rtl"; michael@0: michael@0: ViewportHandler.setMetadataForDocument(this.browser.contentDocument, aMetadata); michael@0: this.sendViewportMetadata(); michael@0: michael@0: this.updateViewportSize(gScreenWidth, aInitialLoad); michael@0: }, michael@0: michael@0: /** Update viewport when the metadata or the window size changes. */ michael@0: updateViewportSize: function updateViewportSize(aOldScreenWidth, aInitialLoad) { michael@0: // When this function gets called on window resize, we must execute michael@0: // this.sendViewportUpdate() so that refreshDisplayPort is called. michael@0: // Ensure that when making changes to this function that code path michael@0: // is not accidentally removed (the call to sendViewportUpdate() is michael@0: // at the very end). michael@0: michael@0: if (this.viewportMeasureCallback) { michael@0: clearTimeout(this.viewportMeasureCallback); michael@0: this.viewportMeasureCallback = null; michael@0: } michael@0: michael@0: let browser = this.browser; michael@0: if (!browser) michael@0: return; michael@0: michael@0: let screenW = gScreenWidth - gViewportMargins.left - gViewportMargins.right; michael@0: let screenH = gScreenHeight - gViewportMargins.top - gViewportMargins.bottom; michael@0: let viewportW, viewportH; michael@0: michael@0: let metadata = this.metadata; michael@0: if (metadata.autoSize) { michael@0: viewportW = screenW / window.devicePixelRatio; michael@0: viewportH = screenH / window.devicePixelRatio; michael@0: } else { michael@0: viewportW = metadata.width; michael@0: viewportH = metadata.height; michael@0: michael@0: // If (scale * width) < device-width, increase the width (bug 561413). michael@0: let maxInitialZoom = metadata.defaultZoom || metadata.maxZoom; michael@0: if (maxInitialZoom && viewportW) { michael@0: viewportW = Math.max(viewportW, screenW / maxInitialZoom); michael@0: } michael@0: michael@0: let validW = viewportW > 0; michael@0: let validH = viewportH > 0; michael@0: michael@0: if (!validW) michael@0: viewportW = validH ? (viewportH * (screenW / screenH)) : BrowserApp.defaultBrowserWidth; michael@0: if (!validH) michael@0: viewportH = viewportW * (screenH / screenW); michael@0: } michael@0: michael@0: // Make sure the viewport height is not shorter than the window when michael@0: // the page is zoomed out to show its full width. Note that before michael@0: // we set the viewport width, the "full width" of the page isn't properly michael@0: // defined, so that's why we have to call setBrowserSize twice - once michael@0: // to set the width, and the second time to figure out the height based michael@0: // on the layout at that width. michael@0: let oldBrowserWidth = this.browserWidth; michael@0: this.setBrowserSize(viewportW, viewportH); michael@0: michael@0: // This change to the zoom accounts for all types of changes I can conceive: michael@0: // 1. screen size changes, CSS viewport does not (pages with no meta viewport michael@0: // or a fixed size viewport) michael@0: // 2. screen size changes, CSS viewport also does (pages with a device-width michael@0: // viewport) michael@0: // 3. screen size remains constant, but CSS viewport changes (meta viewport michael@0: // tag is added or removed) michael@0: // 4. neither screen size nor CSS viewport changes michael@0: // michael@0: // In all of these cases, we maintain how much actual content is visible michael@0: // within the screen width. Note that "actual content" may be different michael@0: // with respect to CSS pixels because of the CSS viewport size changing. michael@0: let zoom = this.restoredSessionZoom() || metadata.defaultZoom; michael@0: if (!zoom || !aInitialLoad) { michael@0: let zoomScale = (screenW * oldBrowserWidth) / (aOldScreenWidth * viewportW); michael@0: zoom = this.clampZoom(this._zoom * zoomScale); michael@0: } michael@0: this.setResolution(zoom, false); michael@0: this.setScrollClampingSize(zoom); michael@0: michael@0: // if this page has not been painted yet, then this must be getting run michael@0: // because a meta-viewport element was added (via the DOMMetaAdded handler). michael@0: // in this case, we should not do anything that forces a reflow (see bug 759678) michael@0: // such as requesting the page size or sending a viewport update. this code michael@0: // will get run again in the before-first-paint handler and that point we michael@0: // will run though all of it. the reason we even bother executing up to this michael@0: // point on the DOMMetaAdded handler is so that scripts that use window.innerWidth michael@0: // before they are painted have a correct value (bug 771575). michael@0: if (!this.contentDocumentIsDisplayed) { michael@0: return; michael@0: } michael@0: michael@0: this.viewportExcludesHorizontalMargins = true; michael@0: this.viewportExcludesVerticalMargins = true; michael@0: let minScale = 1.0; michael@0: if (this.browser.contentDocument) { michael@0: // this may get run during a Viewport:Change message while the document michael@0: // has not yet loaded, so need to guard against a null document. michael@0: let [pageWidth, pageHeight] = this.getPageSize(this.browser.contentDocument, viewportW, viewportH); michael@0: michael@0: // In the situation the page size equals or exceeds the screen size, michael@0: // lengthen the viewport on the corresponding axis to include the margins. michael@0: // The '- 0.5' is to account for rounding errors. michael@0: if (pageWidth * this._zoom > gScreenWidth - 0.5) { michael@0: screenW = gScreenWidth; michael@0: this.viewportExcludesHorizontalMargins = false; michael@0: } michael@0: if (pageHeight * this._zoom > gScreenHeight - 0.5) { michael@0: screenH = gScreenHeight; michael@0: this.viewportExcludesVerticalMargins = false; michael@0: } michael@0: michael@0: minScale = screenW / pageWidth; michael@0: } michael@0: minScale = this.clampZoom(minScale); michael@0: viewportH = Math.max(viewportH, screenH / minScale); michael@0: michael@0: // In general we want to keep calls to setBrowserSize and setScrollClampingSize michael@0: // together because setBrowserSize could mark the viewport size as dirty, creating michael@0: // a pending resize event for content. If that resize gets dispatched (which happens michael@0: // on the next reflow) without setScrollClampingSize having being called, then michael@0: // content might be exposed to incorrect innerWidth/innerHeight values. michael@0: this.setBrowserSize(viewportW, viewportH); michael@0: this.setScrollClampingSize(zoom); michael@0: michael@0: // Avoid having the scroll position jump around after device rotation. michael@0: let win = this.browser.contentWindow; michael@0: this.userScrollPos.x = win.scrollX; michael@0: this.userScrollPos.y = win.scrollY; michael@0: michael@0: this.sendViewportUpdate(); michael@0: michael@0: if (metadata.allowZoom && !Services.prefs.getBoolPref("browser.ui.zoom.force-user-scalable")) { michael@0: // If the CSS viewport is narrower than the screen (i.e. width <= device-width) michael@0: // then we disable double-tap-to-zoom behaviour. michael@0: var oldAllowDoubleTapZoom = metadata.allowDoubleTapZoom; michael@0: var newAllowDoubleTapZoom = (!metadata.isSpecified) || (viewportW > screenW / window.devicePixelRatio); michael@0: if (oldAllowDoubleTapZoom !== newAllowDoubleTapZoom) { michael@0: metadata.allowDoubleTapZoom = newAllowDoubleTapZoom; michael@0: this.sendViewportMetadata(); michael@0: } michael@0: } michael@0: michael@0: // Store the page size that was used to calculate the viewport so that we michael@0: // can verify it's changed when we consider remeasuring in updateViewportForPageSize michael@0: let viewport = this.getViewport(); michael@0: this.lastPageSizeAfterViewportRemeasure = { michael@0: width: viewport.pageRight - viewport.pageLeft, michael@0: height: viewport.pageBottom - viewport.pageTop michael@0: }; michael@0: }, michael@0: michael@0: sendViewportMetadata: function sendViewportMetadata() { michael@0: let metadata = this.metadata; michael@0: sendMessageToJava({ michael@0: type: "Tab:ViewportMetadata", michael@0: allowZoom: metadata.allowZoom, michael@0: allowDoubleTapZoom: metadata.allowDoubleTapZoom, michael@0: defaultZoom: metadata.defaultZoom || window.devicePixelRatio, michael@0: minZoom: metadata.minZoom || 0, michael@0: maxZoom: metadata.maxZoom || 0, michael@0: isRTL: metadata.isRTL, michael@0: tabID: this.id michael@0: }); michael@0: }, michael@0: michael@0: setBrowserSize: function(aWidth, aHeight) { michael@0: if (fuzzyEquals(this.browserWidth, aWidth) && fuzzyEquals(this.browserHeight, aHeight)) { michael@0: return; michael@0: } michael@0: michael@0: this.browserWidth = aWidth; michael@0: this.browserHeight = aHeight; michael@0: michael@0: if (!this.browser.contentWindow) michael@0: return; michael@0: let cwu = this.browser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils); michael@0: cwu.setCSSViewport(aWidth, aHeight); michael@0: }, michael@0: michael@0: /** Takes a scale and restricts it based on this tab's zoom limits. */ michael@0: clampZoom: function clampZoom(aZoom) { michael@0: let zoom = ViewportHandler.clamp(aZoom, kViewportMinScale, kViewportMaxScale); michael@0: michael@0: let md = this.metadata; michael@0: if (!md.allowZoom) michael@0: return md.defaultZoom || zoom; michael@0: michael@0: if (md && md.minZoom) michael@0: zoom = Math.max(zoom, md.minZoom); michael@0: if (md && md.maxZoom) michael@0: zoom = Math.min(zoom, md.maxZoom); michael@0: return zoom; michael@0: }, michael@0: michael@0: observe: function(aSubject, aTopic, aData) { michael@0: switch (aTopic) { michael@0: case "before-first-paint": michael@0: // Is it on the top level? michael@0: let contentDocument = aSubject; michael@0: if (contentDocument == this.browser.contentDocument) { michael@0: if (BrowserApp.selectedTab == this) { michael@0: BrowserApp.contentDocumentChanged(); michael@0: } michael@0: this.contentDocumentIsDisplayed = true; michael@0: michael@0: // reset CSS viewport and zoom to default on new page, and then calculate michael@0: // them properly using the actual metadata from the page. note that the michael@0: // updateMetadata call takes into account the existing CSS viewport size michael@0: // and zoom when calculating the new ones, so we need to reset these michael@0: // things here before calling updateMetadata. michael@0: this.setBrowserSize(kDefaultCSSViewportWidth, kDefaultCSSViewportHeight); michael@0: let zoom = this.restoredSessionZoom() || gScreenWidth / this.browserWidth; michael@0: this.setResolution(zoom, true); michael@0: ViewportHandler.updateMetadata(this, true); michael@0: michael@0: // Note that if we draw without a display-port, things can go wrong. By the michael@0: // time we execute this, it's almost certain a display-port has been set via michael@0: // the MozScrolledAreaChanged event. If that didn't happen, the updateMetadata michael@0: // call above does so at the end of the updateViewportSize function. As long michael@0: // as that is happening, we don't need to do it again here. michael@0: michael@0: if (!this.restoredSessionZoom() && contentDocument.mozSyntheticDocument) { michael@0: // for images, scale to fit width. this needs to happen *after* the call michael@0: // to updateMetadata above, because that call sets the CSS viewport which michael@0: // will affect the page size (i.e. contentDocument.body.scroll*) that we michael@0: // use in this calculation. also we call sendViewportUpdate after changing michael@0: // the resolution so that the display port gets recalculated appropriately. michael@0: let fitZoom = Math.min(gScreenWidth / contentDocument.body.scrollWidth, michael@0: gScreenHeight / contentDocument.body.scrollHeight); michael@0: this.setResolution(fitZoom, false); michael@0: this.sendViewportUpdate(); michael@0: } michael@0: } michael@0: michael@0: // If the reflow-text-on-page-load pref is enabled, and reflow-on-zoom michael@0: // is enabled, and our defaultZoom level is set, then we need to get michael@0: // the default zoom and reflow the text according to the defaultZoom michael@0: // level. michael@0: let rzEnabled = BrowserEventHandler.mReflozPref; michael@0: let rzPl = Services.prefs.getBoolPref("browser.zoom.reflowZoom.reflowTextOnPageLoad"); michael@0: michael@0: if (rzEnabled && rzPl) { michael@0: // Retrieve the viewport width and adjust the max line box width michael@0: // accordingly. michael@0: let vp = BrowserApp.selectedTab.getViewport(); michael@0: BrowserApp.selectedTab.performReflowOnZoom(vp); michael@0: } michael@0: break; michael@0: case "after-viewport-change": michael@0: if (BrowserApp.selectedTab._mReflozPositioned) { michael@0: BrowserApp.selectedTab.clearReflowOnZoomPendingActions(); michael@0: } michael@0: break; michael@0: case "nsPref:changed": michael@0: if (aData == "browser.ui.zoom.force-user-scalable") michael@0: ViewportHandler.updateMetadata(this, false); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: set readerEnabled(isReaderEnabled) { michael@0: this._readerEnabled = isReaderEnabled; michael@0: if (this.getActive()) michael@0: Reader.updatePageAction(this); michael@0: }, michael@0: michael@0: get readerEnabled() { michael@0: return this._readerEnabled; michael@0: }, michael@0: michael@0: set readerActive(isReaderActive) { michael@0: this._readerActive = isReaderActive; michael@0: if (this.getActive()) michael@0: Reader.updatePageAction(this); michael@0: }, michael@0: michael@0: get readerActive() { michael@0: return this._readerActive; michael@0: }, michael@0: michael@0: // nsIBrowserTab michael@0: get window() { michael@0: if (!this.browser) michael@0: return null; michael@0: return this.browser.contentWindow; michael@0: }, michael@0: michael@0: get scale() { michael@0: return this._zoom; michael@0: }, michael@0: michael@0: QueryInterface: XPCOMUtils.generateQI([ michael@0: Ci.nsIWebProgressListener, michael@0: Ci.nsISHistoryListener, michael@0: Ci.nsIObserver, michael@0: Ci.nsISupportsWeakReference, michael@0: Ci.nsIBrowserTab michael@0: ]) michael@0: }; michael@0: michael@0: var BrowserEventHandler = { michael@0: init: function init() { michael@0: Services.obs.addObserver(this, "Gesture:SingleTap", false); michael@0: Services.obs.addObserver(this, "Gesture:CancelTouch", false); michael@0: Services.obs.addObserver(this, "Gesture:DoubleTap", false); michael@0: Services.obs.addObserver(this, "Gesture:Scroll", false); michael@0: Services.obs.addObserver(this, "dom-touch-listener-added", false); michael@0: michael@0: BrowserApp.deck.addEventListener("DOMUpdatePageReport", PopupBlockerObserver.onUpdatePageReport, false); michael@0: BrowserApp.deck.addEventListener("touchstart", this, true); michael@0: BrowserApp.deck.addEventListener("click", InputWidgetHelper, true); michael@0: BrowserApp.deck.addEventListener("click", SelectHelper, true); michael@0: michael@0: SpatialNavigation.init(BrowserApp.deck, null); michael@0: michael@0: document.addEventListener("MozMagnifyGesture", this, true); michael@0: michael@0: Services.prefs.addObserver("browser.zoom.reflowOnZoom", this, false); michael@0: this.updateReflozPref(); michael@0: }, michael@0: michael@0: resetMaxLineBoxWidth: function() { michael@0: BrowserApp.selectedTab.probablyNeedRefloz = false; michael@0: michael@0: if (gReflowPending) { michael@0: clearTimeout(gReflowPending); michael@0: } michael@0: michael@0: let reflozTimeout = Services.prefs.getIntPref("browser.zoom.reflowZoom.reflowTimeout"); michael@0: gReflowPending = setTimeout(doChangeMaxLineBoxWidth, michael@0: reflozTimeout, 0); michael@0: }, michael@0: michael@0: updateReflozPref: function() { michael@0: this.mReflozPref = Services.prefs.getBoolPref("browser.zoom.reflowOnZoom"); michael@0: }, michael@0: michael@0: handleEvent: function(aEvent) { michael@0: switch (aEvent.type) { michael@0: case 'touchstart': michael@0: this._handleTouchStart(aEvent); michael@0: break; michael@0: case 'MozMagnifyGesture': michael@0: this.observe(this, aEvent.type, michael@0: JSON.stringify({x: aEvent.screenX, y: aEvent.screenY, michael@0: zoomDelta: aEvent.delta})); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: _handleTouchStart: function(aEvent) { michael@0: if (!BrowserApp.isBrowserContentDocumentDisplayed() || aEvent.touches.length > 1 || aEvent.defaultPrevented) michael@0: return; michael@0: michael@0: let closest = aEvent.target; michael@0: michael@0: if (closest) { michael@0: // If we've pressed a scrollable element, let Java know that we may michael@0: // want to override the scroll behaviour (for document sub-frames) michael@0: this._scrollableElement = this._findScrollableElement(closest, true); michael@0: this._firstScrollEvent = true; michael@0: michael@0: if (this._scrollableElement != null) { michael@0: // Discard if it's the top-level scrollable, we let Java handle this michael@0: // The top-level scrollable is the body in quirks mode and the html element michael@0: // in standards mode michael@0: let doc = BrowserApp.selectedBrowser.contentDocument; michael@0: let rootScrollable = (doc.compatMode === "BackCompat" ? doc.body : doc.documentElement); michael@0: if (this._scrollableElement != rootScrollable) { michael@0: sendMessageToJava({ type: "Panning:Override" }); michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (!ElementTouchHelper.isElementClickable(closest, null, false)) michael@0: closest = ElementTouchHelper.elementFromPoint(aEvent.changedTouches[0].screenX, michael@0: aEvent.changedTouches[0].screenY); michael@0: if (!closest) michael@0: closest = aEvent.target; michael@0: michael@0: if (closest) { michael@0: let uri = this._getLinkURI(closest); michael@0: if (uri) { michael@0: try { michael@0: Services.io.QueryInterface(Ci.nsISpeculativeConnect).speculativeConnect(uri, null); michael@0: } catch (e) {} michael@0: } michael@0: this._doTapHighlight(closest); michael@0: } michael@0: }, michael@0: michael@0: _getLinkURI: function(aElement) { michael@0: if (aElement.nodeType == Ci.nsIDOMNode.ELEMENT_NODE && michael@0: ((aElement instanceof Ci.nsIDOMHTMLAnchorElement && aElement.href) || michael@0: (aElement instanceof Ci.nsIDOMHTMLAreaElement && aElement.href))) { michael@0: try { michael@0: return Services.io.newURI(aElement.href, null, null); michael@0: } catch (e) {} michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: observe: function(aSubject, aTopic, aData) { michael@0: if (aTopic == "dom-touch-listener-added") { michael@0: let tab = BrowserApp.getTabForWindow(aSubject.top); michael@0: if (!tab || tab.hasTouchListener) michael@0: return; michael@0: michael@0: tab.hasTouchListener = true; michael@0: sendMessageToJava({ michael@0: type: "Tab:HasTouchListener", michael@0: tabID: tab.id michael@0: }); michael@0: return; michael@0: } else if (aTopic == "nsPref:changed") { michael@0: if (aData == "browser.zoom.reflowOnZoom") { michael@0: this.updateReflozPref(); michael@0: } michael@0: return; michael@0: } michael@0: michael@0: // the remaining events are all dependent on the browser content document being the michael@0: // same as the browser displayed document. if they are not the same, we should ignore michael@0: // the event. michael@0: if (BrowserApp.isBrowserContentDocumentDisplayed()) { michael@0: this.handleUserEvent(aTopic, aData); michael@0: } michael@0: }, michael@0: michael@0: handleUserEvent: function(aTopic, aData) { michael@0: switch (aTopic) { michael@0: michael@0: case "Gesture:Scroll": { michael@0: // If we've lost our scrollable element, return. Don't cancel the michael@0: // override, as we probably don't want Java to handle panning until the michael@0: // user releases their finger. michael@0: if (this._scrollableElement == null) michael@0: return; michael@0: michael@0: // If this is the first scroll event and we can't scroll in the direction michael@0: // the user wanted, and neither can any non-root sub-frame, cancel the michael@0: // override so that Java can handle panning the main document. michael@0: let data = JSON.parse(aData); michael@0: michael@0: // round the scroll amounts because they come in as floats and might be michael@0: // subject to minor rounding errors because of zoom values. I've seen values michael@0: // like 0.99 come in here and get truncated to 0; this avoids that problem. michael@0: let zoom = BrowserApp.selectedTab._zoom; michael@0: let x = Math.round(data.x / zoom); michael@0: let y = Math.round(data.y / zoom); michael@0: michael@0: if (this._firstScrollEvent) { michael@0: while (this._scrollableElement != null && michael@0: !this._elementCanScroll(this._scrollableElement, x, y)) michael@0: this._scrollableElement = this._findScrollableElement(this._scrollableElement, false); michael@0: michael@0: let doc = BrowserApp.selectedBrowser.contentDocument; michael@0: if (this._scrollableElement == null || michael@0: this._scrollableElement == doc.documentElement) { michael@0: sendMessageToJava({ type: "Panning:CancelOverride" }); michael@0: return; michael@0: } michael@0: michael@0: this._firstScrollEvent = false; michael@0: } michael@0: michael@0: // Scroll the scrollable element michael@0: if (this._elementCanScroll(this._scrollableElement, x, y)) { michael@0: this._scrollElementBy(this._scrollableElement, x, y); michael@0: sendMessageToJava({ type: "Gesture:ScrollAck", scrolled: true }); michael@0: SelectionHandler.subdocumentScrolled(this._scrollableElement); michael@0: } else { michael@0: sendMessageToJava({ type: "Gesture:ScrollAck", scrolled: false }); michael@0: } michael@0: michael@0: break; michael@0: } michael@0: michael@0: case "Gesture:CancelTouch": michael@0: this._cancelTapHighlight(); michael@0: break; michael@0: michael@0: case "Gesture:SingleTap": { michael@0: let element = this._highlightElement; michael@0: if (element) { michael@0: try { michael@0: let data = JSON.parse(aData); michael@0: let [x, y] = [data.x, data.y]; michael@0: if (ElementTouchHelper.isElementClickable(element)) { michael@0: [x, y] = this._moveClickPoint(element, x, y); michael@0: } michael@0: michael@0: // Was the element already focused before it was clicked? michael@0: let isFocused = (element == BrowserApp.getFocusedInput(BrowserApp.selectedBrowser)); michael@0: michael@0: this._sendMouseEvent("mousemove", element, x, y); michael@0: this._sendMouseEvent("mousedown", element, x, y); michael@0: this._sendMouseEvent("mouseup", element, x, y); michael@0: michael@0: // If the element was previously focused, show the caret attached to it. michael@0: if (isFocused) michael@0: SelectionHandler.attachCaret(element); michael@0: michael@0: // scrollToFocusedInput does its own checks to find out if an element should be zoomed into michael@0: BrowserApp.scrollToFocusedInput(BrowserApp.selectedBrowser); michael@0: } catch(e) { michael@0: Cu.reportError(e); michael@0: } michael@0: } michael@0: this._cancelTapHighlight(); michael@0: break; michael@0: } michael@0: michael@0: case"Gesture:DoubleTap": michael@0: this._cancelTapHighlight(); michael@0: this.onDoubleTap(aData); michael@0: break; michael@0: michael@0: case "MozMagnifyGesture": michael@0: this.onPinchFinish(aData); michael@0: break; michael@0: michael@0: default: michael@0: dump('BrowserEventHandler.handleUserEvent: unexpected topic "' + aTopic + '"'); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: onDoubleTap: function(aData) { michael@0: let data = JSON.parse(aData); michael@0: let element = ElementTouchHelper.anyElementFromPoint(data.x, data.y); michael@0: michael@0: // We only want to do this if reflow-on-zoom is enabled, we don't already michael@0: // have a reflow-on-zoom event pending, and the element upon which the user michael@0: // double-tapped isn't of a type we want to avoid reflow-on-zoom. michael@0: if (BrowserEventHandler.mReflozPref && michael@0: !BrowserApp.selectedTab._mReflozPoint && michael@0: !this._shouldSuppressReflowOnZoom(element)) { michael@0: michael@0: // See comment above performReflowOnZoom() for a detailed description of michael@0: // the events happening in the reflow-on-zoom operation. michael@0: let data = JSON.parse(aData); michael@0: let zoomPointX = data.x; michael@0: let zoomPointY = data.y; michael@0: michael@0: BrowserApp.selectedTab._mReflozPoint = { x: zoomPointX, y: zoomPointY, michael@0: range: BrowserApp.selectedBrowser.contentDocument.caretPositionFromPoint(zoomPointX, zoomPointY) }; michael@0: michael@0: // Before we perform a reflow on zoom, let's disable painting. michael@0: let webNav = BrowserApp.selectedTab.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation); michael@0: let docShell = webNav.QueryInterface(Ci.nsIDocShell); michael@0: let docViewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer); michael@0: docViewer.pausePainting(); michael@0: michael@0: BrowserApp.selectedTab.probablyNeedRefloz = true; michael@0: } michael@0: michael@0: if (!element) { michael@0: ZoomHelper.zoomOut(); michael@0: return; michael@0: } michael@0: michael@0: while (element && !this._shouldZoomToElement(element)) michael@0: element = element.parentNode; michael@0: michael@0: if (!element) { michael@0: ZoomHelper.zoomOut(); michael@0: } else { michael@0: ZoomHelper.zoomToElement(element, data.y); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Determine if reflow-on-zoom functionality should be suppressed, given a michael@0: * particular element. Double-tapping on the following elements suppresses michael@0: * reflow-on-zoom: michael@0: * michael@0: *