diff --git a/browser/metro/Makefile.in b/browser/metro/Makefile.in
new file mode 100644
index 000000000000..cd6c34e7f688
--- /dev/null
+++ b/browser/metro/Makefile.in
@@ -0,0 +1,51 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEPTH = ../..
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+include $(topsrcdir)/config/config.mk
+
+DIRS = \
+ base \
+ components \
+ modules \
+ theme \
+ profile \
+ locales \
+ $(NULL)
+
+ifeq ($(OS_ARCH),WINNT)
+DIRS += shell
+endif
+
+TEST_DIRS += base/tests
+
+include $(topsrcdir)/config/rules.mk
+
+#########################################
+# application.ini
+
+ifdef MOZILLA_OFFICIAL
+DEFINES += -DMOZILLA_OFFICIAL
+endif
+
+GRE_MILESTONE := $(shell tail -n 1 $(topsrcdir)/config/milestone.txt 2>/dev/null || tail -1 $(topsrcdir)/config/milestone.txt)
+GRE_BUILDID := $(shell cat $(DEPTH)/config/buildid)
+DEFINES += -DGRE_MILESTONE=$(GRE_MILESTONE) -DGRE_BUILDID=$(GRE_BUILDID)
+
+# 'application.ini' breaks firefox build config. So we use something different.
+metroapp.ini: metroapp.ini.in $(DEPTH)/config/buildid $(topsrcdir)/config/milestone.txt
+ $(RM) "metroapp.ini"
+ $(PYTHON) $(topsrcdir)/config/Preprocessor.py $(DEFINES) $< > $@
+
+libs:: metroapp.ini
+ $(INSTALL) metroapp.ini $(FINAL_TARGET)
+
+#########################################
+
+GARBAGE += metroapp.ini
diff --git a/browser/metro/base/Makefile.in b/browser/metro/base/Makefile.in
new file mode 100644
index 000000000000..6cf5adc48bd5
--- /dev/null
+++ b/browser/metro/base/Makefile.in
@@ -0,0 +1,20 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEPTH = ../../..
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+include $(topsrcdir)/config/config.mk
+
+DIST_SUBDIR=metro
+
+DEFINES += -DAB_CD=$(MOZ_UI_LOCALE) \
+ -DPACKAGE=browser \
+ -DMOZ_APP_VERSION=$(MOZ_APP_VERSION) \
+ $(NULL)
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/metro/base/content/AnimatedZoom.js b/browser/metro/base/content/AnimatedZoom.js
new file mode 100644
index 000000000000..44108fe3b41d
--- /dev/null
+++ b/browser/metro/base/content/AnimatedZoom.js
@@ -0,0 +1,124 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Responsible for zooming in to a given view rectangle
+ */
+const AnimatedZoom = {
+ startScale: null,
+
+ /** Starts an animated zoom to zoomRect. */
+ animateTo: function(aZoomRect) {
+ if (!aZoomRect)
+ return;
+
+ this.zoomTo = aZoomRect.clone();
+
+ if (this.animationDuration === undefined)
+ this.animationDuration = Services.prefs.getIntPref("browser.ui.zoom.animationDuration");
+
+ Browser.forceChromeReflow();
+
+ this.start();
+
+ // Check if zooming animations were occuring before.
+ if (!this.zoomRect) {
+ this.updateTo(this.zoomFrom);
+
+ mozRequestAnimationFrame(this);
+
+ let event = document.createEvent("Events");
+ event.initEvent("AnimatedZoomBegin", true, true);
+ window.dispatchEvent(event);
+ }
+ },
+
+ start: function start() {
+ this.tab = Browser.selectedTab;
+ this.browser = this.tab.browser;
+ this.bcr = this.browser.getBoundingClientRect();
+ this.zoomFrom = this.zoomRect || this.getStartRect();
+ this.startScale = this.browser.scale;
+ this.beginTime = mozAnimationStartTime;
+ },
+
+ /** Get the visible rect, in device pixels relative to the content origin. */
+ getStartRect: function getStartRect() {
+ let browser = this.browser;
+ let scroll = browser.getRootView().getPosition();
+ return new Rect(scroll.x, scroll.y, this.bcr.width, this.bcr.height);
+ },
+
+ /** Update the visible rect, in device pixels relative to the content origin. */
+ updateTo: function(nextRect) {
+ // Stop animating if the browser has been destroyed
+ if (typeof this.browser.fuzzyZoom !== "function") {
+ this.reset();
+ return false;
+ }
+
+ let zoomRatio = this.bcr.width / nextRect.width;
+ let scale = this.startScale * zoomRatio;
+ let scrollX = nextRect.left * zoomRatio;
+ let scrollY = nextRect.top * zoomRatio;
+
+ this.browser.fuzzyZoom(scale, scrollX, scrollY);
+
+ this.zoomRect = nextRect;
+ return true;
+ },
+
+ /** Stop animation, zoom to point, and clean up. */
+ finish: function() {
+ if (!this.updateTo(this.zoomTo || this.zoomRect))
+ return;
+
+ // Check whether the zoom limits have changed since the animation started.
+ let browser = this.browser;
+ let finalScale = this.tab.clampZoomLevel(browser.scale);
+ if (browser.scale != finalScale)
+ browser.scale = finalScale; // scale= calls finishFuzzyZoom.
+ else
+ browser.finishFuzzyZoom();
+
+ this.reset();
+ browser._updateCSSViewport();
+ },
+
+ reset: function reset() {
+ this.beginTime = null;
+ this.zoomTo = null;
+ this.zoomFrom = null;
+ this.zoomRect = null;
+ this.startScale = null;
+
+ let event = document.createEvent("Events");
+ event.initEvent("AnimatedZoomEnd", true, true);
+ window.dispatchEvent(event);
+ },
+
+ isZooming: function isZooming() {
+ return this.beginTime != null;
+ },
+
+ sample: function(aTimeStamp) {
+ try {
+ let tdiff = aTimeStamp - this.beginTime;
+ let counter = tdiff / this.animationDuration;
+ if (counter < 1) {
+ // update browser to interpolated rectangle
+ let rect = this.zoomFrom.blend(this.zoomTo, counter);
+ if (this.updateTo(rect))
+ mozRequestAnimationFrame(this);
+ } else {
+ // last cycle already rendered final scaled image, now clean up
+ this.finish();
+ }
+ } catch(e) {
+ this.finish();
+ throw e;
+ }
+ }
+};
diff --git a/browser/metro/base/content/BrowserTouchHandler.js b/browser/metro/base/content/BrowserTouchHandler.js
new file mode 100644
index 000000000000..f88e9252a9c5
--- /dev/null
+++ b/browser/metro/base/content/BrowserTouchHandler.js
@@ -0,0 +1,91 @@
+/*
+ * BrowserTouchHandler
+ *
+ * Receives touch events from our input event capturing in input.js
+ * and relays appropriate events to content. Also handles requests
+ * from content for UI.
+ */
+
+const BrowserTouchHandler = {
+ _debugEvents: false,
+
+ init: function init() {
+ // Misc. events we catch during the bubbling phase
+ document.addEventListener("PopupChanged", this, false);
+ document.addEventListener("CancelTouchSequence", this, false);
+
+ // Messages sent from content.js
+ messageManager.addMessageListener("Content:ContextMenu", this);
+ },
+
+ // Content forwarding the contextmenu command
+ onContentContextMenu: function onContentContextMenu(aMessage) {
+ let contextInfo = { name: aMessage.name,
+ json: aMessage.json,
+ target: aMessage.target };
+ // Touch input selection handling
+ if (!InputSourceHelper.isPrecise) {
+ if (SelectionHelperUI.isActive()) {
+ // Selection handler is active.
+ if (aMessage.json.types.indexOf("selected-text") != -1) {
+ // long tap on existing selection. The incoming message has the
+ // string data, so reset the selection handler and invoke the
+ // context menu.
+ SelectionHelperUI.closeEditSession();
+ } else {
+ // Weird, context menu request with no selected text and
+ // SelectionHelperUI is active? Might be a bug, warn. Fall
+ // through anyway, the context menu handler will look in the
+ // incoming message for content types it knows how to handle.
+ Util.dumpLn("long tap on empty selection with SelectionHelperUI active?");
+ SelectionHelperUI.closeEditSession();
+ }
+ } else if (SelectionHelperUI.canHandle(aMessage)) {
+ SelectionHelperUI.openEditSession(aMessage);
+ return;
+ }
+ }
+
+ // Check to see if we have context menu item(s) that apply to what
+ // was clicked on.
+ if (ContextMenuUI.showContextMenu(contextInfo)) {
+ let event = document.createEvent("Events");
+ event.initEvent("CancelTouchSequence", true, false);
+ document.dispatchEvent(event);
+ } else {
+ // Send the MozEdgeUIGesture to input.js to
+ // toggle the context ui.
+ let event = document.createEvent("Events");
+ event.initEvent("MozEdgeUIGesture", true, false);
+ window.dispatchEvent(event);
+ }
+ },
+
+ /*
+ * Events
+ */
+
+ handleEvent: function handleEvent(aEvent) {
+ // ignore content events we generate
+ if (this._debugEvents)
+ Util.dumpLn("BrowserTouchHandler:", aEvent.type);
+
+ switch (aEvent.type) {
+ case "PopupChanged":
+ case "CancelTouchSequence":
+ if (!aEvent.detail)
+ ContextMenuUI.reset();
+ break;
+ }
+ },
+
+ receiveMessage: function receiveMessage(aMessage) {
+ if (this._debugEvents) Util.dumpLn("BrowserTouchHandler:", aMessage.name);
+ switch (aMessage.name) {
+ // Content forwarding the contextmenu command
+ case "Content:ContextMenu":
+ this.onContentContextMenu(aMessage);
+ break;
+ }
+ },
+};
diff --git a/browser/metro/base/content/ContextCommands.js b/browser/metro/base/content/ContextCommands.js
new file mode 100644
index 000000000000..d149421f397f
--- /dev/null
+++ b/browser/metro/base/content/ContextCommands.js
@@ -0,0 +1,301 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+ /*
+ * context menu command handlers
+ */
+
+var ContextCommands = {
+ _picker: null,
+
+ get clipboard() {
+ return Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper);
+ },
+
+ get docRef() {
+ return Browser.selectedBrowser.contentWindow.document;
+ },
+
+ /*
+ * Context menu handlers
+ */
+
+ copy: function cc_copy() {
+ let target = ContextMenuUI.popupState.target;
+ if (target.localName == "browser") {
+ if (ContextMenuUI.popupState.string != "undefined") {
+ this.clipboard.copyString(ContextMenuUI.popupState.string,
+ this.docRef);
+ } else {
+ let x = ContextMenuUI.popupState.x;
+ let y = ContextMenuUI.popupState.y;
+ let json = {x: x, y: y, command: "copy" };
+ target.messageManager.sendAsyncMessage("Browser:ContextCommand", json);
+ }
+ } else {
+ target.editor.copy();
+ }
+
+ if (target)
+ target.focus();
+ },
+
+ paste: function cc_paste() {
+ let target = ContextMenuUI.popupState.target;
+ if (target.localName == "browser") {
+ let x = ContextMenuUI.popupState.x;
+ let y = ContextMenuUI.popupState.y;
+ let json = {x: x, y: y, command: "paste" };
+ target.messageManager.sendAsyncMessage("Browser:ContextCommand", json);
+ } else {
+ target.editor.paste(Ci.nsIClipboard.kGlobalClipboard);
+ target.focus();
+ }
+ },
+
+ pasteAndGo: function cc_pasteAndGo() {
+ let target = ContextMenuUI.popupState.target;
+ target.editor.selectAll();
+ target.editor.paste(Ci.nsIClipboard.kGlobalClipboard);
+ BrowserUI.goToURI();
+ },
+
+ selectAll: function cc_selectAll() {
+ let target = ContextMenuUI.popupState.target;
+ if (target.localName == "browser") {
+ let x = ContextMenuUI.popupState.x;
+ let y = ContextMenuUI.popupState.y;
+ let json = {x: x, y: y, command: "select-all" };
+ target.messageManager.sendAsyncMessage("Browser:ContextCommand", json);
+ } else {
+ target.editor.selectAll();
+ target.focus();
+ }
+ },
+
+ openInNewTab: function cc_openInNewTab() {
+ Browser.addTab(ContextMenuUI.popupState.linkURL, false, Browser.selectedTab);
+ ContextUI.peekTabs();
+ },
+
+ saveToWinLibrary: function cc_saveToWinLibrary(aType) {
+ let popupState = ContextMenuUI.popupState;
+ let browser = popupState.target;
+
+ // ContentAreaUtils internalSave relies on various desktop related prefs,
+ // values, and functionality. We want to be more direct by saving the
+ // image to the users Windows Library. If users want to specify the
+ // save location they can use the context menu option 'Save (type) To'.
+
+ let dirSvc = Components.classes["@mozilla.org/file/directory_service;1"]
+ .getService(Components.interfaces.nsIProperties);
+ let saveLocationPath = dirSvc.get(aType, Components.interfaces.nsIFile);
+
+ let fileInfo = new ContentAreaUtils.FileInfo();
+ ContentAreaUtils.initFileInfo(fileInfo, popupState.mediaURL,
+ browser.documentURI.originCharset,
+ null, null, null);
+ let filename =
+ ContentAreaUtils.getNormalizedLeafName(fileInfo.fileName, fileInfo.fileExt);
+ saveLocationPath.append(filename);
+
+ // Start the background save process
+ ContentAreaUtils.internalPersist({
+ sourceURI : fileInfo.uri,
+ sourceDocument : null,
+ sourceReferrer : browser.documentURI,
+ targetContentType : popupState.contentType,
+ targetFile : saveLocationPath,
+ sourceCacheKey : null,
+ sourcePostData : null,
+ bypassCache : false,
+ initiatingWindow : this.docRef.defaultView
+ });
+ },
+
+ saveVideo: function cc_saveVideo() {
+ this.saveToWinLibrary("Vids");
+ },
+
+ saveVideoTo: function cc_saveVideoTo() {
+ this.saveFileAs(ContextMenuUI.popupState);
+ },
+
+ saveImage: function cc_saveImage() {
+ this.saveToWinLibrary("Pict");
+ },
+
+ saveImageTo: function cc_saveImageTo() {
+ this.saveFileAs(ContextMenuUI.popupState);
+ },
+
+ copyLink: function cc_copyLink() {
+ this.clipboard.copyString(ContextMenuUI.popupState.linkURL,
+ this.docRef);
+ },
+
+ copyEmail: function cc_copyEmail() {
+ this.clipboard.copyString(ContextMenuUI.popupState.linkURL.substr(ContextMenuUI.popupState.linkURL.indexOf(':')+1),
+ this.docRef);
+ },
+
+ copyPhone: function cc_copyPhone() {
+ this.clipboard.copyString(ContextMenuUI.popupState.linkURL.substr(ContextMenuUI.popupState.linkURL.indexOf(':')+1),
+ this.docRef);
+ },
+
+ copyImageLocation: function cc_copyImageLocation() {
+ this.clipboard.copyString(ContextMenuUI.popupState.mediaURL,
+ this.docRef);
+ },
+
+ bookmarkLink: function cc_bookmarkLink() {
+ let state = ContextMenuUI.popupState;
+ let uri = Util.makeURI(state.linkURL);
+ let title = state.linkTitle || state.linkURL;
+
+ try {
+ Bookmarks.addForURI(uri, title);
+ } catch (e) {
+ return;
+ }
+
+ let message = Strings.browser.GetStringFromName("alertLinkBookmarked");
+ let toaster = Cc["@mozilla.org/toaster-alerts-service;1"].getService(Ci.nsIAlertsService);
+ toaster.showAlertNotification(null, message, "", false, "", null);
+ },
+
+ sendCommand: function cc_playVideo(aCommand) {
+ let browser = ContextMenuUI.popupState.target;
+ browser.messageManager.sendAsyncMessage("Browser:ContextCommand", { command: aCommand });
+ },
+
+ editBookmark: function cc_editBookmark() {
+ let target = ContextMenuUI.popupState.target;
+ target.startEditing();
+ },
+
+ removeBookmark: function cc_removeBookmark() {
+ let target = ContextMenuUI.popupState.target;
+ target.remove();
+ },
+
+ shortcutBookmark: function cc_shortcutBookmark() {
+ let target = ContextMenuUI.popupState.target;
+ Util.createShortcut(target.getAttribute("title"), target.getAttribute("uri"), target.getAttribute("src"), "bookmark");
+ },
+
+ findInPage: function cc_findInPage() {
+ dump('ContextCommand: findInPage');
+ FindHelperUI.show();
+ },
+
+ viewOnDesktop: function cc_viewOnDesktop() {
+ dump('ContextCommand: viewOnDesktop');
+ Appbar.onViewOnDesktop();
+ },
+
+ /*
+ * Utilities
+ */
+
+ /*
+ * isAccessibleDirectory
+ *
+ * Test to see if the directory exists and is writable.
+ */
+ isAccessibleDirectory: function isAccessibleDirectory(aDirectory) {
+ return aDirectory && aDirectory.exists() && aDirectory.isDirectory() &&
+ aDirectory.isWritable();
+ },
+
+ /*
+ * saveFileAs
+ *
+ * Browse for a location and then save a file to the local system using
+ * standard download prefs.
+ *
+ * @param aFileUriString path to file
+ */
+ saveFileAs: function saveFileAs(aPopupState) {
+ let srcUri = null;
+ let mediaURL = aPopupState.mediaURL;
+ try {
+ srcUri = Util.makeURI(mediaURL, null, null);
+ } catch (ex) {
+ Util.dumpLn("could not parse:", mediaURL);
+ return;
+ }
+
+ let picker = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ let windowTitle = Strings.browser.GetStringFromName("browserForSaveLocation");
+
+ picker.init(window, windowTitle, Ci.nsIFilePicker.modeSave);
+
+ // prefered filename
+ let fileName = mediaURL.substring(mediaURL.lastIndexOf("/") + 1);
+ if (fileName.length)
+ picker.defaultString = fileName;
+
+ // prefered file extension
+ let fileExtension = mediaURL.substring(mediaURL.lastIndexOf(".") + 1);
+ if (fileExtension.length)
+ picker.defaultExtension = fileExtension;
+ picker.appendFilters(Ci.nsIFilePicker.filterImages);
+
+ // prefered save location
+ var dnldMgr = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
+ picker.displayDirectory = dnldMgr.userDownloadsDirectory;
+ try {
+ let lastDir = Services.prefs.getComplexValue("browser.download.lastDir", Ci.nsILocalFile);
+ if (this.isAccessibleDirectory(lastDir))
+ picker.displayDirectory = lastDir;
+ }
+ catch (e) { }
+
+ this._picker = picker;
+ this._pickerUrl = mediaURL;
+ this._pickerContentDisp = aPopupState.contentDisposition;
+ this._contentType = aPopupState.contentType;
+ picker.open(ContextCommands);
+ },
+
+ /*
+ * Filepicker callback
+ */
+ done: function done(aSuccess) {
+ let picker = this._picker;
+ this._picker = null;
+
+ if (aSuccess == Ci.nsIFilePicker.returnCancel)
+ return;
+
+ let file = picker.file;
+ if (file == null)
+ return;
+
+ try {
+ if (file.exists())
+ file.remove(false);
+ } catch (e) {}
+
+ ContentAreaUtils.internalSave(this._pickerUrl, null, null,
+ this._pickerContentDisp,
+ this._contentType, false, "SaveImageTitle",
+ new AutoChosen(file, Util.makeURI(this._pickerUrl, null, null)),
+ getBrowser().documentURI, this.docRef, true, null);
+
+ var newDir = file.parent.QueryInterface(Ci.nsILocalFile);
+ Services.prefs.setComplexValue("browser.download.lastDir", Ci.nsILocalFile, newDir);
+ },
+
+};
+
+function AutoChosen(aFileAutoChosen, aUriAutoChosen) {
+ this.file = aFileAutoChosen;
+ this.uri = aUriAutoChosen;
+}
+
diff --git a/browser/metro/base/content/LoginManagerChild.js b/browser/metro/base/content/LoginManagerChild.js
new file mode 100644
index 000000000000..0e601800f86b
--- /dev/null
+++ b/browser/metro/base/content/LoginManagerChild.js
@@ -0,0 +1,818 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+var loginManager = {
+
+ /* ---------- private members ---------- */
+
+
+ get _formFillService() {
+ return this._formFillService =
+ Cc["@mozilla.org/satchel/form-fill-controller;1"].
+ getService(Ci.nsIFormFillController);
+ },
+
+ _nsLoginInfo : null, // Constructor for nsILoginInfo implementation
+ _debug : false, // mirrors signon.debug
+ _remember : true, // mirrors signon.rememberSignons preference
+
+ init : function () {
+ // Cache references to current |this| in utility objects
+ this._webProgressListener._domEventListener = this._domEventListener;
+ this._webProgressListener._pwmgr = this;
+ this._domEventListener._pwmgr = this;
+ this._observer._pwmgr = this;
+
+ // Form submit observer checks forms for new logins and pw changes.
+ Services.obs.addObserver(this._observer, "earlyformsubmit", false);
+
+ // Get constructor for nsILoginInfo
+ this._nsLoginInfo = new Components.Constructor(
+ "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo);
+
+ // Preferences. Add observer so we get notified of changes.
+ Services.prefs.addObserver("signon.", this._observer, false);
+
+ // Get current preference values.
+ this._debug = Services.prefs.getBoolPref("signon.debug");
+ this._remember = Services.prefs.getBoolPref("signon.rememberSignons");
+
+ // WebProgressListener for getting notification of new doc loads.
+ var progress = Cc["@mozilla.org/docloaderservice;1"].
+ getService(Ci.nsIWebProgress);
+ progress.addProgressListener(this._webProgressListener,
+ Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT);
+ },
+
+
+ /*
+ * log
+ *
+ * Internal function for logging debug messages to the Error Console window
+ */
+ log : function (message) {
+ if (!this._debug)
+ return;
+ dump("PasswordUtils: " + message + "\n");
+ Services.console.logStringMessage("PasswordUtils: " + message);
+ },
+
+
+ /* fillForm
+ *
+ * Fill the form with login information if we can find it.
+ */
+ fillForm: function (form) {
+ this._fillForm(form, true, true, false, null);
+ },
+
+
+ /*
+ * _fillform
+ *
+ * Fill the form with login information if we can find it.
+ * autofillForm denotes if we should fill the form in automatically
+ * ignoreAutocomplete denotes if we should ignore autocomplete=off attributes
+ * foundLogins is an array of nsILoginInfo
+ */
+ _fillForm : function (form, autofillForm, ignoreAutocomplete,
+ clobberPassword, foundLogins) {
+ // Heuristically determine what the user/pass fields are
+ // We do this before checking to see if logins are stored,
+ // so that the user isn't prompted for a master password
+ // without need.
+ var [usernameField, passwordField, ignored] =
+ this._getFormFields(form, false);
+
+ // Need a valid password field to do anything.
+ if (passwordField == null)
+ return false;
+
+ // If the fields are disabled or read-only, there's nothing to do.
+ if (passwordField.disabled || passwordField.readOnly ||
+ usernameField && (usernameField.disabled ||
+ usernameField.readOnly)) {
+ this.log("not filling form, login fields disabled");
+ return false;
+ }
+
+ // Discard logins which have username/password values that don't
+ // fit into the fields (as specified by the maxlength attribute).
+ // The user couldn't enter these values anyway, and it helps
+ // with sites that have an extra PIN to be entered (bug 391514)
+ var maxUsernameLen = Number.MAX_VALUE;
+ var maxPasswordLen = Number.MAX_VALUE;
+
+ // If attribute wasn't set, default is -1.
+ if (usernameField && usernameField.maxLength >= 0)
+ maxUsernameLen = usernameField.maxLength;
+ if (passwordField.maxLength >= 0)
+ maxPasswordLen = passwordField.maxLength;
+
+ var logins = foundLogins.filter(function (l) {
+ var fit = (l.username.length <= maxUsernameLen &&
+ l.password.length <= maxPasswordLen);
+ if (!fit)
+ this.log("Ignored " + l.username + " login: won't fit");
+
+ return fit;
+ }, this);
+
+
+ // Nothing to do if we have no matching logins available.
+ if (logins.length == 0)
+ return false;
+
+
+ // The reason we didn't end up filling the form, if any. We include
+ // this in the formInfo object we send with the passwordmgr-found-logins
+ // notification. See the _notifyFoundLogins docs for possible values.
+ var didntFillReason = null;
+
+ // Attach autocomplete stuff to the username field, if we have
+ // one. This is normally used to select from multiple accounts,
+ // but even with one account we should refill if the user edits.
+ if (usernameField)
+ this._attachToInput(usernameField);
+
+ // Don't clobber an existing password.
+ if (passwordField.value && !clobberPassword) {
+ didntFillReason = "existingPassword";
+ this._notifyFoundLogins(didntFillReason, usernameField,
+ passwordField, foundLogins, null);
+ return false;
+ }
+
+ // If the form has an autocomplete=off attribute in play, don't
+ // fill in the login automatically. We check this after attaching
+ // the autocomplete stuff to the username field, so the user can
+ // still manually select a login to be filled in.
+ var isFormDisabled = false;
+ if (!ignoreAutocomplete &&
+ (this._isAutocompleteDisabled(form) ||
+ this._isAutocompleteDisabled(usernameField) ||
+ this._isAutocompleteDisabled(passwordField))) {
+
+ isFormDisabled = true;
+ this.log("form not filled, has autocomplete=off");
+ }
+
+ // Variable such that we reduce code duplication and can be sure we
+ // should be firing notifications if and only if we can fill the form.
+ var selectedLogin = null;
+
+ if (usernameField && usernameField.value) {
+ // If username was specified in the form, only fill in the
+ // password if we find a matching login.
+ var username = usernameField.value.toLowerCase();
+
+ let matchingLogins = logins.filter(function(l)
+ l.username.toLowerCase() == username);
+ if (matchingLogins.length) {
+ selectedLogin = matchingLogins[0];
+ } else {
+ didntFillReason = "existingUsername";
+ this.log("Password not filled. None of the stored " +
+ "logins match the username already present.");
+ }
+ } else if (logins.length == 1) {
+ selectedLogin = logins[0];
+ } else {
+ // We have multiple logins. Handle a special case here, for sites
+ // which have a normal user+pass login *and* a password-only login
+ // (eg, a PIN). Prefer the login that matches the type of the form
+ // (user+pass or pass-only) when there's exactly one that matches.
+ let matchingLogins;
+ if (usernameField)
+ matchingLogins = logins.filter(function(l) l.username);
+ else
+ matchingLogins = logins.filter(function(l) !l.username);
+ if (matchingLogins.length == 1) {
+ selectedLogin = matchingLogins[0];
+ } else {
+ didntFillReason = "multipleLogins";
+ this.log("Multiple logins for form, so not filling any.");
+ }
+ }
+
+ var didFillForm = false;
+ if (selectedLogin && autofillForm && !isFormDisabled) {
+ // Fill the form
+ if (usernameField)
+ usernameField.value = selectedLogin.username;
+ passwordField.value = selectedLogin.password;
+ didFillForm = true;
+ } else if (selectedLogin && !autofillForm) {
+ // For when autofillForm is false, but we still have the information
+ // to fill a form, we notify observers.
+ didntFillReason = "noAutofillForms";
+ Services.obs.notifyObservers(form, "passwordmgr-found-form", didntFillReason);
+ this.log("autofillForms=false but form can be filled; notified observers");
+ } else if (selectedLogin && isFormDisabled) {
+ // For when autocomplete is off, but we still have the information
+ // to fill a form, we notify observers.
+ didntFillReason = "autocompleteOff";
+ Services.obs.notifyObservers(form, "passwordmgr-found-form", didntFillReason);
+ this.log("autocomplete=off but form can be filled; notified observers");
+ }
+
+ this._notifyFoundLogins(didntFillReason, usernameField, passwordField,
+ foundLogins, selectedLogin);
+
+ return didFillForm;
+ },
+
+
+ /*
+ * _getPasswordOrigin
+ *
+ * Get the parts of the URL we want for identification.
+ */
+ _getPasswordOrigin : function (uriString, allowJS) {
+ var realm = "";
+ try {
+ var uri = Services.io.newURI(uriString, null, null);
+
+ if (allowJS && uri.scheme == "javascript")
+ return "javascript:"
+
+ realm = uri.scheme + "://" + uri.host;
+
+ // If the URI explicitly specified a port, only include it when
+ // it's not the default. (We never want "http://foo.com:80")
+ var port = uri.port;
+ if (port != -1) {
+ var handler = Services.io.getProtocolHandler(uri.scheme);
+ if (port != handler.defaultPort)
+ realm += ":" + port;
+ }
+
+ } catch (e) {
+ // bug 159484 - disallow url types that don't support a hostPort.
+ // (although we handle "javascript:..." as a special case above.)
+ this.log("Couldn't parse origin for " + uriString);
+ realm = null;
+ }
+
+ return realm;
+ },
+
+
+ _getActionOrigin : function (form) {
+ var uriString = form.action;
+
+ // A blank or mission action submits to where it came from.
+ if (uriString == "")
+ uriString = form.baseURI; // ala bug 297761
+
+ return this._getPasswordOrigin(uriString, true);
+ },
+
+
+ /*
+ * _isAutoCompleteDisabled
+ *
+ * Returns true if the page requests autocomplete be disabled for the
+ * specified form input.
+ */
+ _isAutocompleteDisabled : function (element) {
+ if (element && element.hasAttribute("autocomplete") &&
+ element.getAttribute("autocomplete").toLowerCase() == "off")
+ return true;
+
+ return false;
+ },
+
+
+ /*
+ * _getFormFields
+ *
+ * Returns the username and password fields found in the form.
+ * Can handle complex forms by trying to figure out what the
+ * relevant fields are.
+ *
+ * Returns: [usernameField, newPasswordField, oldPasswordField]
+ *
+ * usernameField may be null.
+ * newPasswordField will always be non-null.
+ * oldPasswordField may be null. If null, newPasswordField is just
+ * "theLoginField". If not null, the form is apparently a
+ * change-password field, with oldPasswordField containing the password
+ * that is being changed.
+ */
+ _getFormFields : function (form, isSubmission) {
+ var usernameField = null;
+
+ // Locate the password field(s) in the form. Up to 3 supported.
+ // If there's no password field, there's nothing for us to do.
+ var pwFields = this._getPasswordFields(form, isSubmission);
+ if (!pwFields)
+ return [null, null, null];
+
+
+ // Locate the username field in the form by searching backwards
+ // from the first passwordfield, assume the first text field is the
+ // username. We might not find a username field if the user is
+ // already logged in to the site.
+ for (var i = pwFields[0].index - 1; i >= 0; i--) {
+ var element = form.elements[i];
+ var fieldType = (element.hasAttribute("type") ?
+ element.getAttribute("type").toLowerCase() :
+ element.type);
+ if (fieldType == "text" ||
+ fieldType == "email" ||
+ fieldType == "url" ||
+ fieldType == "tel" ||
+ fieldType == "number") {
+ usernameField = element;
+ break;
+ }
+ }
+
+ if (!usernameField)
+ this.log("(form -- no username field found)");
+
+
+ // If we're not submitting a form (it's a page load), there are no
+ // password field values for us to use for identifying fields. So,
+ // just assume the first password field is the one to be filled in.
+ if (!isSubmission || pwFields.length == 1)
+ return [usernameField, pwFields[0].element, null];
+
+
+ // Try to figure out WTF is in the form based on the password values.
+ var oldPasswordField, newPasswordField;
+ var pw1 = pwFields[0].element.value;
+ var pw2 = pwFields[1].element.value;
+ var pw3 = (pwFields[2] ? pwFields[2].element.value : null);
+
+ if (pwFields.length == 3) {
+ // Look for two identical passwords, that's the new password
+
+ if (pw1 == pw2 && pw2 == pw3) {
+ // All 3 passwords the same? Weird! Treat as if 1 pw field.
+ newPasswordField = pwFields[0].element;
+ oldPasswordField = null;
+ } else if (pw1 == pw2) {
+ newPasswordField = pwFields[0].element;
+ oldPasswordField = pwFields[2].element;
+ } else if (pw2 == pw3) {
+ oldPasswordField = pwFields[0].element;
+ newPasswordField = pwFields[2].element;
+ } else if (pw1 == pw3) {
+ // A bit odd, but could make sense with the right page layout.
+ newPasswordField = pwFields[0].element;
+ oldPasswordField = pwFields[1].element;
+ } else {
+ // We can't tell which of the 3 passwords should be saved.
+ this.log("(form ignored -- all 3 pw fields differ)");
+ return [null, null, null];
+ }
+ } else { // pwFields.length == 2
+ if (pw1 == pw2) {
+ // Treat as if 1 pw field
+ newPasswordField = pwFields[0].element;
+ oldPasswordField = null;
+ } else {
+ // Just assume that the 2nd password is the new password
+ oldPasswordField = pwFields[0].element;
+ newPasswordField = pwFields[1].element;
+ }
+ }
+
+ return [usernameField, newPasswordField, oldPasswordField];
+ },
+
+
+ /* ---------- Private methods ---------- */
+
+
+ /*
+ * _getPasswordFields
+ *
+ * Returns an array of password field elements for the specified form.
+ * If no pw fields are found, or if more than 3 are found, then null
+ * is returned.
+ *
+ * skipEmptyFields can be set to ignore password fields with no value.
+ */
+ _getPasswordFields : function (form, skipEmptyFields) {
+ // Locate the password fields in the form.
+ var pwFields = [];
+ for (var i = 0; i < form.elements.length; i++) {
+ var element = form.elements[i];
+ if (!(element instanceof Ci.nsIDOMHTMLInputElement) ||
+ element.type != "password")
+ continue;
+
+ if (skipEmptyFields && !element.value)
+ continue;
+
+ pwFields[pwFields.length] = {
+ index : i,
+ element : element
+ };
+ }
+
+ // If too few or too many fields, bail out.
+ if (pwFields.length == 0) {
+ this.log("(form ignored -- no password fields.)");
+ return null;
+ } else if (pwFields.length > 3) {
+ this.log("(form ignored -- too many password fields. [got " +
+ pwFields.length + "])");
+ return null;
+ }
+
+ return pwFields;
+ },
+
+
+ /**
+ * Notify observers about an attempt to fill a form that resulted in some
+ * saved logins being found for the form.
+ *
+ * This does not get called if the login manager attempts to fill a form
+ * but does not find any saved logins. It does, however, get called when
+ * the login manager does find saved logins whether or not it actually
+ * fills the form with one of them.
+ *
+ * @param didntFillReason {String}
+ * the reason the login manager didn't fill the form, if any;
+ * if the value of this parameter is null, then the form was filled;
+ * otherwise, this parameter will be one of these values:
+ * existingUsername: the username field already contains a username
+ * that doesn't match any stored usernames
+ * existingPassword: the password field already contains a password
+ * autocompleteOff: autocomplete has been disabled for the form
+ * or its username or password fields
+ * multipleLogins: we have multiple logins for the form
+ * noAutofillForms: the autofillForms pref is set to false
+ *
+ * @param usernameField {HTMLInputElement}
+ * the username field detected by the login manager, if any;
+ * otherwise null
+ *
+ * @param passwordField {HTMLInputElement}
+ * the password field detected by the login manager
+ *
+ * @param foundLogins {Array}
+ * an array of nsILoginInfos that can be used to fill the form
+ *
+ * @param selectedLogin {nsILoginInfo}
+ * the nsILoginInfo that was/would be used to fill the form, if any;
+ * otherwise null; whether or not it was actually used depends on
+ * the value of the didntFillReason parameter
+ */
+ _notifyFoundLogins : function (didntFillReason, usernameField,
+ passwordField, foundLogins, selectedLogin) {
+ // We need .setProperty(), which is a method on the original
+ // nsIWritablePropertyBag. Strangley enough, nsIWritablePropertyBag2
+ // doesn't inherit from that, so the additional QI is needed.
+ let formInfo = Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(Ci.nsIWritablePropertyBag2).
+ QueryInterface(Ci.nsIWritablePropertyBag);
+
+ formInfo.setPropertyAsACString("didntFillReason", didntFillReason);
+ formInfo.setPropertyAsInterface("usernameField", usernameField);
+ formInfo.setPropertyAsInterface("passwordField", passwordField);
+ formInfo.setProperty("foundLogins", foundLogins.concat());
+ formInfo.setPropertyAsInterface("selectedLogin", selectedLogin);
+
+ Services.obs.notifyObservers(formInfo, "passwordmgr-found-logins", null);
+ },
+
+ /*
+ * _attachToInput
+ *
+ * Hooks up autocomplete support to a username field, to allow
+ * a user editing the field to select an existing login and have
+ * the password field filled in.
+ */
+ _attachToInput : function (element) {
+ this.log("attaching autocomplete stuff");
+ element.addEventListener("blur",
+ this._domEventListener, false);
+ element.addEventListener("DOMAutoComplete",
+ this._domEventListener, false);
+ this._formFillService.markAsLoginManagerField(element);
+ },
+
+ /*
+ * _fillDocument
+ *
+ * Called when a page has loaded. For each form in the document,
+ * we ask the parent process to see if it can be filled with a stored login
+ * and fill them in with the results
+ */
+ _fillDocument : function (doc) {
+ var forms = doc.forms;
+ if (!forms || forms.length == 0)
+ return;
+
+ this.log("_fillDocument processing " + forms.length +
+ " forms on " + doc.documentURI);
+
+ var autofillForm = !PrivateBrowsingUtils.isWindowPrivate(doc.defaultView) &&
+ Services.prefs.getBoolPref("signon.autofillForms");
+
+ // actionOrigins is a list of each form's action origins for this
+ // document. The parent process needs this to find the passwords
+ // for each action origin.
+ var actionOrigins = [];
+
+ for (var i = 0; i < forms.length; i++) {
+ var form = forms[i];
+ let [, passwordField, ] = this._getFormFields(form, false);
+ if (passwordField) {
+ var actionOrigin = this._getActionOrigin(form);
+ actionOrigins.push(actionOrigin);
+ }
+ } // foreach form
+
+ if (!actionOrigins.length)
+ return;
+
+ var formOrigin = this._getPasswordOrigin(doc.documentURI);
+ var foundLogins = this._getPasswords(actionOrigins, formOrigin);
+
+ for (var i = 0; i < forms.length; i++) {
+ var form = forms[i];
+ var actionOrigin = this._getActionOrigin(form);
+ if (foundLogins[actionOrigin]) {
+ this.log("_fillDocument processing form[" + i + "]");
+ this._fillForm(form, autofillForm, false, false,
+ foundLogins[actionOrigin]);
+ }
+ } // foreach form
+ },
+
+ /*
+ * _getPasswords
+ *
+ * Retrieve passwords from parent process and prepare logins to be passed to
+ * _fillForm. Returns map from action origins to passwords.
+ */
+ _getPasswords: function(actionOrigins, formOrigin) {
+ // foundLogins will be a map from action origins to passwords.
+ var message = sendSyncMessage("PasswordMgr:GetPasswords", {
+ actionOrigins: actionOrigins,
+ formOrigin: formOrigin
+ })[0];
+
+ // XXX need to somehow respond to the UI being busy
+ // not needed for Fennec yet
+
+ var foundLogins = message.foundLogins;
+
+ // Each password will be a JSON-unserialized object, but they need to be
+ // nsILoginInfo's.
+ for (var key in foundLogins) {
+ var logins = foundLogins[key];
+ for (var i = 0; i < logins.length; i++) {
+ var obj = logins[i];
+ logins[i] = new this._nsLoginInfo();
+ logins[i].init(obj.hostname, obj.formSubmitURL, obj.httpRealm,
+ obj.username, obj.password,
+ obj.usernameField, obj.passwordField);
+ }
+ }
+
+ return foundLogins;
+ },
+
+ /*
+ * _onFormSubmit
+ *
+ * Called by the our observer when notified of a form submission.
+ * [Note that this happens before any DOM onsubmit handlers are invoked.]
+ * Looks for a password change in the submitted form, so we can update
+ * our stored password.
+ */
+ _onFormSubmit : function (form) {
+ var doc = form.ownerDocument;
+ var win = doc.defaultView;
+
+ if (PrivateBrowsingUtils.isWindowPrivate(win)) {
+ // We won't do anything in private browsing mode anyway,
+ // so there's no need to perform further checks.
+ this.log("(form submission ignored in private browsing mode)");
+ return;
+ }
+
+ // If password saving is disabled (globally or for host), bail out now.
+ if (!this._remember)
+ return;
+
+ var hostname = this._getPasswordOrigin(doc.documentURI);
+ var formSubmitURL = this._getActionOrigin(form);
+
+
+ // Get the appropriate fields from the form.
+ var [usernameField, newPasswordField, oldPasswordField] =
+ this._getFormFields(form, true);
+
+ // Need at least 1 valid password field to do anything.
+ if (newPasswordField == null)
+ return;
+
+ // Check for autocomplete=off attribute. We don't use it to prevent
+ // autofilling (for existing logins), but won't save logins when it's
+ // present.
+ // XXX spin out a bug that we don't update timeLastUsed in this case?
+ if (this._isAutocompleteDisabled(form) ||
+ this._isAutocompleteDisabled(usernameField) ||
+ this._isAutocompleteDisabled(newPasswordField) ||
+ this._isAutocompleteDisabled(oldPasswordField)) {
+ this.log("(form submission ignored -- autocomplete=off found)");
+ return;
+ }
+
+ sendSyncMessage("PasswordMgr:FormSubmitted", {
+ hostname: hostname,
+ formSubmitURL: formSubmitURL,
+ usernameField: usernameField ? usernameField.name : "",
+ usernameValue: usernameField ? usernameField.value : "",
+ passwordField: newPasswordField.name,
+ passwordValue: newPasswordField.value,
+ hasOldPasswordField: !!oldPasswordField
+ });
+ },
+
+ /* ---------- Utility objects ---------- */
+
+ /*
+ * _observer object
+ *
+ * Internal utility object, implements the nsIObserver interface.
+ * Used to receive notification for: form submission, preference changes.
+ */
+ _observer : {
+ _pwmgr : null,
+
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsIFormSubmitObserver,
+ Ci.nsISupportsWeakReference]),
+
+
+ // nsFormSubmitObserver
+ notify : function (formElement, aWindow, actionURI) {
+ // Counterintuitively, form submit observers fire for content that
+ // may not be the content in this context.
+ if (aWindow.top != content)
+ return true;
+
+ this._pwmgr.log("observer notified for form submission.");
+
+ // We're invoked before the content's |onsubmit| handlers, so we
+ // can grab form data before it might be modified (see bug 257781).
+
+ try {
+ this._pwmgr._onFormSubmit(formElement);
+ } catch (e) {
+ this._pwmgr.log("Caught error in onFormSubmit: " + e);
+ }
+
+ return true; // Always return true, or form submit will be canceled.
+ },
+
+ observe : function (aSubject, aTopic, aData) {
+ this._pwmgr._debug = Services.prefs.getBoolPref("signon.debug");
+ this._pwmgr._remember = Services.prefs.getBoolPref("signon.rememberSignons");
+ }
+ },
+
+
+ /*
+ * _webProgressListener object
+ *
+ * Internal utility object, implements nsIWebProgressListener interface.
+ * This is attached to the document loader service, so we get
+ * notifications about all page loads.
+ */
+ _webProgressListener : {
+ _pwmgr : null,
+ _domEventListener : null,
+
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference]),
+
+
+ onStateChange : function (aWebProgress, aRequest,
+ aStateFlags, aStatus) {
+
+ // STATE_START is too early, doc is still the old page.
+ if (!(aStateFlags & Ci.nsIWebProgressListener.STATE_TRANSFERRING))
+ return;
+
+ if (!this._pwmgr._remember)
+ return;
+
+ var domWin = aWebProgress.DOMWindow;
+ var domDoc = domWin.document;
+
+ // Only process things which might have HTML forms.
+ if (!(domDoc instanceof Ci.nsIDOMHTMLDocument))
+ return;
+ if (this._pwmgr._debug) {
+ let requestName = "(null)";
+ if (aRequest) {
+ try {
+ requestName = aRequest.name;
+ } catch (ex if ex.result == Components.results.NS_ERROR_NOT_IMPLEMENTED) {
+ // do nothing - leave requestName = "(null)"
+ }
+ }
+ this._pwmgr.log("onStateChange accepted: req = " + requestName +
+ ", flags = 0x" + aStateFlags.toString(16));
+ }
+
+ // Fastback doesn't fire DOMContentLoaded, so process forms now.
+ if (aStateFlags & Ci.nsIWebProgressListener.STATE_RESTORING) {
+ this._pwmgr.log("onStateChange: restoring document");
+ return this._pwmgr._fillDocument(domDoc);
+ }
+
+ // Add event listener to process page when DOM is complete.
+ domDoc.addEventListener("DOMContentLoaded",
+ this._domEventListener, false);
+ return;
+ },
+
+ // stubs for the nsIWebProgressListener interfaces which we don't use.
+ onProgressChange : function() { throw "Unexpected onProgressChange"; },
+ onLocationChange : function() { throw "Unexpected onLocationChange"; },
+ onStatusChange : function() { throw "Unexpected onStatusChange"; },
+ onSecurityChange : function() { throw "Unexpected onSecurityChange"; }
+ },
+
+ /*
+ * _domEventListener object
+ *
+ * Internal utility object, implements nsIDOMEventListener
+ * Used to catch certain DOM events needed to properly implement form fill.
+ */
+ _domEventListener : {
+ _pwmgr : null,
+
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIDOMEventListener,
+ Ci.nsISupportsWeakReference]),
+
+
+ handleEvent : function (event) {
+ if (!event.isTrusted)
+ return;
+
+ this._pwmgr.log("domEventListener: got event " + event.type);
+
+ switch (event.type) {
+ case "DOMAutoComplete":
+ case "blur":
+ var acInputField = event.target;
+ var acForm = acInputField.form;
+
+ // If the username is blank, bail out now -- we don't want
+ // _fillForm() to try filling in a login without a username
+ // to filter on (bug 471906).
+ if (!acInputField.value)
+ return;
+
+ // Make sure the username field _fillForm will use is the
+ // same field as the autocomplete was activated on. If
+ // not, the DOM has been altered and we'll just give up.
+ var [usernameField, passwordField, ignored] =
+ this._pwmgr._getFormFields(acForm, false);
+ if (usernameField == acInputField && passwordField) {
+ var actionOrigin = this._pwmgr._getActionOrigin(acForm);
+ var formOrigin = this._pwmgr._getPasswordOrigin(acForm.ownerDocument.documentURI);
+ var foundLogins = this._pwmgr._getPasswords([actionOrigin], formOrigin);
+ this._pwmgr._fillForm(acForm, true, true, true, foundLogins[actionOrigin]);
+ } else {
+ this._pwmgr.log("Oops, form changed before AC invoked");
+ }
+ return;
+
+ case "DOMContentLoaded":
+ // Only process when we need to
+ event.currentTarget.removeEventListener(event.type, this);
+ if (this._pwmgr._remember && event.target instanceof Ci.nsIDOMHTMLDocument)
+ this._pwmgr._fillDocument(event.target);
+ break;
+
+ default:
+ this._pwmgr.log("Oops! This event unexpected.");
+ return;
+ }
+ }
+ }
+};
+
+loginManager.init();
diff --git a/browser/metro/base/content/PageActions.js b/browser/metro/base/content/PageActions.js
new file mode 100644
index 000000000000..7a14df29112e
--- /dev/null
+++ b/browser/metro/base/content/PageActions.js
@@ -0,0 +1,180 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var PageActions = {
+ _handlers: null,
+
+ init: function init() {
+ if (this._handlers)
+ return;
+
+ this._handlers = [];
+ document.getElementById("pageactions-container").addEventListener("click", this, true);
+
+ this.register("pageaction-reset", this.updatePagePermissions, this);
+ this.register("pageaction-password", this.updateForgetPassword, this);
+#ifdef NS_PRINTING
+ this.register("pageaction-saveas", this.updatePageSaveAs, this);
+#endif
+ this.register("pageaction-share", this.updateShare, this);
+
+ CharsetMenu.init();
+ },
+
+ handleEvent: function handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "click":
+ IdentityUI.hide();
+ break;
+ }
+ },
+
+ /**
+ * @param aId id of a pageaction element
+ * @param aCallback function that takes an element and returns true if it should be visible
+ * @param aThisObj (optional) scope object for aCallback
+ */
+ register: function register(aId, aCallback, aThisObj) {
+ this.init();
+ this._handlers.push({id: aId, callback: aCallback, obj: aThisObj});
+ },
+
+ updateSiteMenu: function updateSiteMenu() {
+ this.init();
+ this._handlers.forEach(function(action) {
+ let node = document.getElementById(action.id);
+ if (node)
+ node.hidden = !action.callback.call(action.obj, node);
+ });
+ this._updateAttributes();
+ },
+
+ get _loginManager() {
+ delete this._loginManager;
+ return this._loginManager = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
+ },
+
+ // Permissions we track in Page Actions
+ _permissions: ["popup", "offline-app", "geolocation", "desktop-notification", "openWebappsManage"],
+
+ _forEachPermissions: function _forEachPermissions(aHost, aCallback) {
+ let pm = Services.perms;
+ for (let i = 0; i < this._permissions.length; i++) {
+ let type = this._permissions[i];
+ if (!pm.testPermission(aHost, type))
+ continue;
+
+ let perms = pm.enumerator;
+ while (perms.hasMoreElements()) {
+ let permission = perms.getNext().QueryInterface(Ci.nsIPermission);
+ if (permission.host == aHost.asciiHost && permission.type == type)
+ aCallback(type);
+ }
+ }
+ },
+
+ updatePagePermissions: function updatePagePermissions(aNode) {
+ let host = Browser.selectedBrowser.currentURI;
+ let permissions = [];
+
+ this._forEachPermissions(host, function(aType) {
+ permissions.push("pageactions." + aType);
+ });
+
+ if (!this._loginManager.getLoginSavingEnabled(host.prePath)) {
+ // If rememberSignons is false, then getLoginSavingEnabled returns false
+ // for all pages, so we should just ignore it (Bug 601163).
+ if (Services.prefs.getBoolPref("signon.rememberSignons"))
+ permissions.push("pageactions.password");
+ }
+
+ let descriptions = permissions.map(function(s) Strings.browser.GetStringFromName(s));
+ aNode.setAttribute("description", descriptions.join(", "));
+
+ return (permissions.length > 0);
+ },
+
+ updateForgetPassword: function updateForgetPassword(aNode) {
+ let host = Browser.selectedBrowser.currentURI;
+ let logins = this._loginManager.findLogins({}, host.prePath, "", "");
+
+ return logins.some(function(login) login.hostname == host.prePath);
+ },
+
+ forgetPassword: function forgetPassword(aEvent) {
+ let host = Browser.selectedBrowser.currentURI;
+ let lm = this._loginManager;
+
+ lm.findLogins({}, host.prePath, "", "").forEach(function(login) {
+ if (login.hostname == host.prePath)
+ lm.removeLogin(login);
+ });
+
+ this.hideItem(aEvent.target);
+ aEvent.stopPropagation(); // Don't hide the site menu.
+ },
+
+ clearPagePermissions: function clearPagePermissions(aEvent) {
+ let pm = Services.perms;
+ let host = Browser.selectedBrowser.currentURI;
+ this._forEachPermissions(host, function(aType) {
+ pm.remove(host.asciiHost, aType);
+
+ // reset the 'remember' counter for permissions that support it
+ if (["geolocation", "desktop-notification"].indexOf(aType) != -1)
+ Services.contentPrefs.setPref(host.asciiHost, aType + ".request.remember", 0);
+ });
+
+ let lm = this._loginManager;
+ if (!lm.getLoginSavingEnabled(host.prePath))
+ lm.setLoginSavingEnabled(host.prePath, true);
+
+ this.hideItem(aEvent.target);
+ aEvent.stopPropagation(); // Don't hide the site menu.
+ },
+
+ pinSite : function pinCurrentSite() {
+ if (Browser.selectedBrowser.currentURI.spec.length == 0) {
+ return;
+ }
+ Browser.pinSite();
+ },
+
+ unpinSite : function unpinCurrentSite() {
+ if (Browser.selectedBrowser.currentURI.spec.length == 0) {
+ return;
+ }
+ Browser.unpinSite();
+ },
+
+ updatePageSaveAs: function updatePageSaveAs(aNode) {
+ // Check for local XUL content
+ let contentWindow = Browser.selectedBrowser.contentWindow;
+ return !(contentWindow && contentWindow.document instanceof XULDocument);
+ },
+
+ updateShare: function updateShare(aNode) {
+ return Util.isShareableScheme(Browser.selectedBrowser.currentURI.scheme);
+ },
+
+ hideItem: function hideItem(aNode) {
+ aNode.hidden = true;
+ this._updateAttributes();
+ },
+
+ _updateAttributes: function _updateAttributes() {
+ let container = document.getElementById("pageactions-container");
+ let visibleNodes = container.querySelectorAll("pageaction:not([hidden=true])");
+ let visibleCount = visibleNodes.length;
+ if (visibleCount == 0)
+ return;
+
+ for (let i = 0; i < visibleCount; i++)
+ visibleNodes[i].classList.remove("odd-last-child");
+
+ visibleNodes[visibleCount - 1].classList.add("last-child");
+ if (visibleCount % 2)
+ visibleNodes[visibleCount - 1].classList.add("odd");
+ }
+};
diff --git a/browser/metro/base/content/RemoteTabs.js b/browser/metro/base/content/RemoteTabs.js
new file mode 100644
index 000000000000..45935c633333
--- /dev/null
+++ b/browser/metro/base/content/RemoteTabs.js
@@ -0,0 +1,144 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+'use strict';
+Components.utils.import("resource://services-sync/main.js");
+
+/**
+ * Wraps a list/grid control implementing nsIDOMXULSelectControlElement and
+ * fills it with the user's synced tabs.
+ *
+ * @param aSet Control implementing nsIDOMXULSelectControlElement.
+ * @param aSetUIAccess The UI element that should be hidden when Sync is
+ * disabled. Must sanely support 'hidden' attribute.
+ * You may only have one UI access point at this time.
+ */
+function RemoteTabsView(aSet, aSetUIAccess) {
+ this._set = aSet;
+ this._set.controller = this;
+ this._uiAccessElement = aSetUIAccess;
+
+ // Sync uses special voodoo observers.
+ // If you want to change this code, talk to the fx-si team
+ Weave.Svc.Obs.add("weave:service:setup-complete", this);
+ Weave.Svc.Obs.add("weave:service:sync:finish", this);
+ Weave.Svc.Obs.add("weave:service:start-over", this);
+ if (this.isSyncEnabled() ) {
+ this.populateTabs();
+ this.populateGrid();
+ this.setUIAccessVisible(true);
+ }
+ else {
+ this.setUIAccessVisible(false);
+ }
+}
+
+RemoteTabsView.prototype = {
+ _set: null,
+ _uiAccessElement: null,
+
+ handleItemClick: function tabview_handleItemClick(aItem) {
+ let url = aItem.getAttribute("value");
+ BrowserUI.goToURI(url);
+ },
+
+ observe: function(subject, topic, data) {
+ switch (topic) {
+ case "weave:service:setup-complete":
+ this.populateTabs();
+ this.setUIAccessVisible(true);
+ break;
+ case "weave:service:sync:finish":
+ this.populateGrid();
+ break;
+ case "weave:service:start-over":
+ this.setUIAccessVisible(false);
+ break;
+ }
+ },
+
+ setUIAccessVisible: function setUIAccessVisible(aVisible) {
+ this._uiAccessElement.hidden = !aVisible;
+ },
+
+ populateGrid: function populateGrid() {
+
+ let tabsEngine = Weave.Service.engineManager.get("tabs");
+ let list = this._set;
+ let seenURLs = new Set();
+
+ for (let [guid, client] in Iterator(tabsEngine.getAllClients())) {
+ client.tabs.forEach(function({title, urlHistory, icon}) {
+ let url = urlHistory[0];
+ if (tabsEngine.locallyOpenTabMatchesURL(url) || seenURLs.has(url)) {
+ return;
+ }
+ seenURLs.add(url);
+
+ // If we wish to group tabs by client, we should be looking for records
+ // of {type:client, clientName, class:{mobile, desktop}} and will
+ // need to readd logic to reset seenURLs for each client.
+
+ let item = this._set.appendItem((title || url), url);
+ item.setAttribute("iconURI", Weave.Utils.getIcon(icon));
+
+ }, this);
+ }
+ },
+
+ populateTabs: function populateTabs() {
+ Weave.Service.scheduler.scheduleNextSync(0);
+ },
+
+ destruct: function destruct() {
+ Weave.Svc.Obs.remove("weave:service:setup-complete", this);
+ Weave.Svc.Obs.remove("weave:engine:sync:finish", this);
+ Weave.Svc.Obs.remove("weave:service:logout:start-over", this);
+ },
+
+ isSyncEnabled: function isSyncEnabled() {
+ return (Weave.Status.checkSetup() != Weave.CLIENT_NOT_CONFIGURED);
+ }
+
+};
+
+let RemoteTabsStartView = {
+ _view: null,
+ get _grid() { return document.getElementById("start-remotetabs-grid"); },
+
+ init: function init() {
+ let vbox = document.getElementById("start-remotetabs");
+ this._view = new RemoteTabsView(this._grid, vbox);
+ },
+
+ uninit: function uninit() {
+ this._view.destruct();
+ },
+
+ show: function show() {
+ this._grid.arrangeItems();
+ }
+};
+
+let RemoteTabsPanelView = {
+ _view: null,
+
+ get _grid() { return document.getElementById("remotetabs-list"); },
+ get visible() { return PanelUI.isPaneVisible("remotetabs-container"); },
+
+ init: function init() {
+ //decks are fragile, don't hide the tab panel(bad things happen), hide link.
+ let menuEntry = document.getElementById("menuitem-remotetabs");
+ this._view = new RemoteTabsView(this._grid, menuEntry);
+ },
+
+ show: function show() {
+ this._grid.arrangeItems();
+ },
+
+ uninit: function uninit() {
+ this._view.destruct();
+ }
+};
diff --git a/browser/metro/base/content/TopSites.js b/browser/metro/base/content/TopSites.js
new file mode 100644
index 000000000000..0de08d98f54d
--- /dev/null
+++ b/browser/metro/base/content/TopSites.js
@@ -0,0 +1,201 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+'use strict';
+ let prefs = Components.classes["@mozilla.org/preferences-service;1"].
+ getService(Components.interfaces.nsIPrefBranch);
+
+// singleton to provide data-level functionality to the views
+let TopSites = {
+ pinSite: function(aId, aSlotIndex) {
+ Util.dumpLn("TopSites.pinSite: " + aId + ", (TODO)");
+ // FIXME: implementation needed
+ return true; // operation was successful
+ },
+ unpinSite: function(aId) {
+ Util.dumpLn("TopSites.unpinSite: " + aId + ", (TODO)");
+ // FIXME: implementation needed
+ return true; // operation was successful
+ },
+ hideSite: function(aId) {
+ Util.dumpLn("TopSites.hideSite: " + aId + ", (TODO)");
+ // FIXME: implementation needed
+ return true; // operation was successful
+ },
+ restoreSite: function(aId) {
+ Util.dumpLn("TopSites.restoreSite: " + aId + ", (TODO)");
+ // FIXME: implementation needed
+ return true; // operation was successful
+ }
+};
+
+function TopSitesView(aGrid, maxSites) {
+ this._set = aGrid;
+ this._set.controller = this;
+ this._topSitesMax = maxSites;
+
+ // handle selectionchange DOM events from the grid/tile group
+ this._set.addEventListener("context-action", this, false);
+}
+
+TopSitesView.prototype = {
+ _set:null,
+ _topSitesMax: null,
+
+ handleItemClick: function tabview_handleItemClick(aItem) {
+ let url = aItem.getAttribute("value");
+ BrowserUI.goToURI(url);
+ },
+
+ doActionOnSelectedTiles: function(aActionName) {
+ let tileGroup = this._set;
+ let selectedTiles = tileGroup.selectedItems;
+
+ switch (aActionName){
+ case "delete":
+ Array.forEach(selectedTiles, function(aNode) {
+ let id = aNode.getAttribute("data-itemid");
+ // add some class to transition element before deletion?
+ if (TopSites.hideSite(id)) {
+ // success
+ aNode.contextActions.delete('delete');
+ aNode.contextActions.add('restore');
+ }
+ // TODO: we'll use some callback/event to remove the item or re-draw the grid
+ });
+ break;
+ case "pin":
+ Array.forEach(selectedTiles, function(aNode) {
+ let id = aNode.getAttribute("data-itemid");
+ if (TopSites.pinSite(id)) {
+ // success
+ aNode.contextActions.delete('pin');
+ aNode.contextActions.add('unpin');
+ }
+ // TODO: we'll use some callback/event to add some class to
+ // indicate element is pinned?
+ });
+ break;
+ case "unpin":
+ Array.forEach(selectedTiles, function(aNode) {
+ let id = aNode.getAttribute("data-itemid");
+ if (TopSites.unpinSite(id)) {
+ // success
+ aNode.contextActions.delete('unpin');
+ aNode.contextActions.add('pin');
+ }
+ // TODO: we'll use some callback/event to add some class to
+ // indicate element is pinned (or just redraw grid)
+ });
+ break;
+ // default: no action
+ }
+ },
+
+ handleEvent: function(aEvent) {
+ switch (aEvent.type){
+ case "context-action":
+ this.doActionOnSelectedTiles(aEvent.action);
+ break;
+ }
+ },
+
+ populateGrid: function populateGrid() {
+ let query = gHistSvc.getNewQuery();
+ let options = gHistSvc.getNewQueryOptions();
+ options.excludeQueries = true;
+ options.queryType = options.QUERY_TYPE_HISTORY;
+ options.maxResults = this._topSitesMax;
+ options.resultType = options.RESULTS_AS_URI;
+ options.sortingMode = options.SORT_BY_FRECENCY_DESCENDING;
+
+ let result = gHistSvc.executeQuery(query, options);
+ let rootNode = result.root;
+ rootNode.containerOpen = true;
+ let childCount = rootNode.childCount;
+
+ // use this property as the data-itemid attribute on the tiles
+ // which identifies the site
+ let identifier = 'uri';
+
+ function isPinned(aNode) {
+ // placeholder condition,
+ // FIXME: do the actual lookup/check
+ return (aNode.uri.indexOf('google') > -1);
+ }
+
+ for (let i = 0; i < childCount; i++) {
+ let node = rootNode.getChild(i);
+ let uri = node.uri;
+ let title = node.title || uri;
+
+ let supportedActions = ['delete'];
+ // placeholder condition - check field/property for this site
+ if (isPinned(node)) {
+ supportedActions.push('unpin');
+ } else {
+ supportedActions.push('pin');
+ }
+ let item = this._set.appendItem(title, uri);
+ item.setAttribute("iconURI", node.icon);
+ item.setAttribute("data-itemid", node[identifier]);
+ // here is where we could add verbs based on pinned etc. state
+ item.setAttribute("data-contextactions", supportedActions.join(','));
+ }
+ rootNode.containerOpen = false;
+ },
+
+ isFirstRun: function isFirstRun() {
+ return prefs.getBoolPref("browser.firstrun.show.localepicker");
+ },
+
+ destruct: function destruct() {
+ // remove the observers here
+ }
+
+};
+
+let TopSitesStartView = {
+ _view: null,
+ get _grid() { return document.getElementById("start-topsites-grid"); },
+
+ init: function init() {
+ this._view = new TopSitesView(this._grid, 9);
+ if (this._view.isFirstRun()) {
+ let topsitesVbox = document.getElementById("start-topsites");
+ topsitesVbox.setAttribute("hidden", "true");
+ }
+ this._view.populateGrid();
+ },
+
+ uninit: function uninit() {
+ this._view.destruct();
+ },
+
+ show: function show() {
+ this._grid.arrangeItems(3, 3);
+ },
+};
+
+let TopSitesSnappedView = {
+ get _grid() { return document.getElementById("snapped-topsite-grid"); },
+
+ show: function show() {
+ this._grid.arrangeItems(1, 9);
+ },
+
+ init: function() {
+ this._view = new TopSitesView(this._grid, 9);
+ if (this._view.isFirstRun()) {
+ let topsitesVbox = document.getElementById("snapped-topsites");
+ topsitesVbox.setAttribute("hidden", "true");
+ }
+ this._view.populateGrid();
+ },
+
+ uninit: function uninit() {
+ this._view.destruct();
+ },
+};
diff --git a/browser/metro/base/content/Util.js b/browser/metro/base/content/Util.js
new file mode 100644
index 000000000000..7cd14250bd7b
--- /dev/null
+++ b/browser/metro/base/content/Util.js
@@ -0,0 +1,412 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+let Util = {
+ /*
+ * General purpose utilities
+ */
+
+ getWindowUtils: function getWindowUtils(aWindow) {
+ return aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ },
+
+ // Recursively find all documents, including root document.
+ getAllDocuments: function getAllDocuments(doc, resultSoFar) {
+ resultSoFar = resultSoFar || [doc];
+ if (!doc.defaultView)
+ return resultSoFar;
+ let frames = doc.defaultView.frames;
+ if (!frames)
+ return resultSoFar;
+
+ let i;
+ let currentDoc;
+ for (i = 0; i < frames.length; i++) {
+ currentDoc = frames[i].document;
+ resultSoFar.push(currentDoc);
+ this.getAllDocuments(currentDoc, resultSoFar);
+ }
+
+ return resultSoFar;
+ },
+
+ // Put the Mozilla networking code into a state that will kick the
+ // auto-connection process.
+ forceOnline: function forceOnline() {
+ Services.io.offline = false;
+ },
+
+ /*
+ * Timing utilties
+ */
+
+ // Executes aFunc after other events have been processed.
+ executeSoon: function executeSoon(aFunc) {
+ Services.tm.mainThread.dispatch({
+ run: function() {
+ aFunc();
+ }
+ }, Ci.nsIThread.DISPATCH_NORMAL);
+ },
+
+ /*
+ * Console printing utilities
+ */
+
+ dumpf: function dumpf(str) {
+ let args = arguments;
+ let i = 1;
+ dump(str.replace(/%s/g, function() {
+ if (i >= args.length) {
+ throw "dumps received too many placeholders and not enough arguments";
+ }
+ return args[i++].toString();
+ }));
+ },
+
+ // Like dump, but each arg is handled and there's an automatic newline
+ dumpLn: function dumpLn() {
+ for (let i = 0; i < arguments.length; i++)
+ dump(arguments[i] + " ");
+ dump("\n");
+ },
+
+ dumpElement: function dumpElement(aElement) {
+ this.dumpLn(aElement.id);
+ },
+
+ dumpElementTree: function dumpElementTree(aElement) {
+ let node = aElement;
+ while (node) {
+ this.dumpLn("node:", node, "id:", node.id, "class:", node.classList);
+ node = node.parentNode;
+ }
+ },
+
+ /*
+ * Element utilities
+ */
+
+ highlightElement: function highlightElement(aElement) {
+ if (aElement == null) {
+ this.dumpLn("aElement is null");
+ return;
+ }
+ aElement.style.border = "2px solid red";
+ },
+
+ getHrefForElement: function getHrefForElement(target) {
+ let link = null;
+ while (target) {
+ if (target instanceof Ci.nsIDOMHTMLAnchorElement ||
+ target instanceof Ci.nsIDOMHTMLAreaElement ||
+ target instanceof Ci.nsIDOMHTMLLinkElement) {
+ if (target.hasAttribute("href"))
+ link = target;
+ }
+ target = target.parentNode;
+ }
+
+ if (link && link.hasAttribute("href"))
+ return link.href;
+ else
+ return null;
+ },
+
+ /*
+ * Rect and nsIDOMRect utilities
+ */
+
+ pointWithinRect: function pointWithinRect(aX, aY, aRect) {
+ return (aRect.left < aX && aRect.top < aY &&
+ aRect.right > aX && aRect.bottom > aY);
+ },
+
+ pointWithinDOMRect: function pointWithinDOMRect(aX, aY, aRect) {
+ if (!aRect.width || !aRect.height)
+ return false;
+ return this.pointWithinRect(aX, aY, aRect);
+ },
+
+ isEmptyDOMRect: function isEmptyDOMRect(aRect) {
+ if ((aRect.bottom - aRect.top) <= 0 &&
+ (aRect.right - aRect.left) <= 0)
+ return true;
+ return false;
+ },
+
+ // Dumps the details of a dom rect to the console
+ dumpDOMRect: function dumpDOMRect(aMsg, aRect) {
+ try {
+ Util.dumpLn(aMsg,
+ "left:" + Math.round(aRect.left) + ",",
+ "top:" + Math.round(aRect.top) + ",",
+ "right:" + Math.round(aRect.right) + ",",
+ "bottom:" + Math.round(aRect.bottom) + ",",
+ "width:" + Math.round(aRect.right - aRect.left) + ",",
+ "height:" + Math.round(aRect.bottom - aRect.top) );
+ } catch (ex) {
+ Util.dumpLn("dumpDOMRect:", ex.message);
+ }
+ },
+
+ /*
+ * URIs and schemes
+ */
+
+ makeURI: function makeURI(aURL, aOriginCharset, aBaseURI) {
+ return Services.io.newURI(aURL, aOriginCharset, aBaseURI);
+ },
+
+ makeURLAbsolute: function makeURLAbsolute(base, url) {
+ // Note: makeURI() will throw if url is not a valid URI
+ return this.makeURI(url, null, this.makeURI(base)).spec;
+ },
+
+ isLocalScheme: function isLocalScheme(aURL) {
+ return ((aURL.indexOf("about:") == 0 &&
+ aURL != "about:blank" &&
+ aURL != "about:empty" &&
+ aURL != "about:start") ||
+ aURL.indexOf("chrome:") == 0);
+ },
+
+ isOpenableScheme: function isShareableScheme(aProtocol) {
+ let dontOpen = /^(mailto|javascript|news|snews)$/;
+ return (aProtocol && !dontOpen.test(aProtocol));
+ },
+
+ isShareableScheme: function isShareableScheme(aProtocol) {
+ let dontShare = /^(chrome|about|file|javascript|resource)$/;
+ return (aProtocol && !dontShare.test(aProtocol));
+ },
+
+ // Don't display anything in the urlbar for these special URIs.
+ isURLEmpty: function isURLEmpty(aURL) {
+ return (!aURL ||
+ aURL == "about:blank" ||
+ aURL == "about:empty" ||
+ aURL == "about:home" ||
+ aURL == "about:start");
+ },
+
+ // Don't remember these pages in the session store.
+ isURLMemorable: function isURLMemorable(aURL) {
+ return !(aURL == "about:blank" ||
+ aURL == "about:empty" ||
+ aURL == "about:start");
+ },
+
+ /*
+ * Math utilities
+ */
+
+ clamp: function(num, min, max) {
+ return Math.max(min, Math.min(max, num));
+ },
+
+ /*
+ * Screen and layout utilities
+ */
+
+ get displayDPI() {
+ delete this.displayDPI;
+ return this.displayDPI = this.getWindowUtils(window).displayDPI;
+ },
+
+ isPortrait: function isPortrait() {
+ return (window.innerWidth <= window.innerHeight);
+ },
+
+ LOCALE_DIR_RTL: -1,
+ LOCALE_DIR_LTR: 1,
+ get localeDir() {
+ // determine browser dir first to know which direction to snap to
+ let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIXULChromeRegistry);
+ return chromeReg.isLocaleRTL("global") ? this.LOCALE_DIR_RTL : this.LOCALE_DIR_LTR;
+ },
+
+ /*
+ * Process utilities
+ */
+
+ isParentProcess: function isInParentProcess() {
+ let appInfo = Cc["@mozilla.org/xre/app-info;1"];
+ return (!appInfo || appInfo.getService(Ci.nsIXULRuntime).processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT);
+ },
+
+ /*
+ * Event utilities
+ */
+
+ modifierMaskFromEvent: function modifierMaskFromEvent(aEvent) {
+ return (aEvent.altKey ? Ci.nsIDOMEvent.ALT_MASK : 0) |
+ (aEvent.ctrlKey ? Ci.nsIDOMEvent.CONTROL_MASK : 0) |
+ (aEvent.shiftKey ? Ci.nsIDOMEvent.SHIFT_MASK : 0) |
+ (aEvent.metaKey ? Ci.nsIDOMEvent.META_MASK : 0);
+ },
+
+ /*
+ * Download utilities
+ */
+
+ insertDownload: function insertDownload(aSrcUri, aFile) {
+ let dm = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
+ let db = dm.DBConnection;
+
+ let stmt = db.createStatement(
+ "INSERT INTO moz_downloads (name, source, target, startTime, endTime, state, referrer) " +
+ "VALUES (:name, :source, :target, :startTime, :endTime, :state, :referrer)"
+ );
+
+ stmt.params.name = aFile.leafName;
+ stmt.params.source = aSrcUri.spec;
+ stmt.params.target = aFile.path;
+ stmt.params.startTime = Date.now() * 1000;
+ stmt.params.endTime = Date.now() * 1000;
+ stmt.params.state = Ci.nsIDownloadManager.DOWNLOAD_NOTSTARTED;
+ stmt.params.referrer = aSrcUri.spec;
+
+ stmt.execute();
+ stmt.finalize();
+
+ let newItemId = db.lastInsertRowID;
+ let download = dm.getDownload(newItemId);
+ //dm.resumeDownload(download);
+ //Services.obs.notifyObservers(download, "dl-start", null);
+ },
+
+ /*
+ * Local system utilities
+ */
+
+ createShortcut: function Util_createShortcut(aTitle, aURL, aIconURL, aType) {
+ // The background images are 72px, but Android will resize as needed.
+ // Bigger is better than too small.
+ const kIconSize = 72;
+ const kOverlaySize = 32;
+ const kOffset = 20;
+
+ // We have to fallback to something
+ aTitle = aTitle || aURL;
+
+ let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+ canvas.setAttribute("style", "display: none");
+
+ function _createShortcut() {
+ let icon = canvas.toDataURL("image/png", "");
+ canvas = null;
+ try {
+ let shell = Cc["@mozilla.org/browser/shell-service;1"].createInstance(Ci.nsIShellService);
+ shell.createShortcut(aTitle, aURL, icon, aType);
+ } catch(e) {
+ Cu.reportError(e);
+ }
+ }
+
+ // Load the main background image first
+ let image = new Image();
+ image.onload = function() {
+ canvas.width = canvas.height = kIconSize;
+ let ctx = canvas.getContext("2d");
+ ctx.drawImage(image, 0, 0, kIconSize, kIconSize);
+
+ // If we have a favicon, lets draw it next
+ if (aIconURL) {
+ let favicon = new Image();
+ favicon.onload = function() {
+ // Center the favicon and overlay it on the background
+ ctx.drawImage(favicon, kOffset, kOffset, kOverlaySize, kOverlaySize);
+ _createShortcut();
+ }
+
+ favicon.onerror = function() {
+ Cu.reportError("CreateShortcut: favicon image load error");
+ }
+
+ favicon.src = aIconURL;
+ } else {
+ _createShortcut();
+ }
+ }
+
+ image.onerror = function() {
+ Cu.reportError("CreateShortcut: background image load error");
+ }
+
+ // Pick the right background
+ image.src = aIconURL ? "chrome://browser/skin/images/homescreen-blank-hdpi.png"
+ : "chrome://browser/skin/images/homescreen-default-hdpi.png";
+ },
+};
+
+
+/*
+ * Timeout
+ *
+ * Helper class to nsITimer that adds a little more pizazz. Callback can be an
+ * object with a notify method or a function.
+ */
+Util.Timeout = function(aCallback) {
+ this._callback = aCallback;
+ this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._type = null;
+};
+
+Util.Timeout.prototype = {
+ // Timer callback. Don't call this manually.
+ notify: function notify() {
+ if (this._type == this._timer.TYPE_ONE_SHOT)
+ this._type = null;
+
+ if (this._callback.notify)
+ this._callback.notify();
+ else
+ this._callback.apply(null);
+ },
+
+ // Helper function for once and interval.
+ _start: function _start(aDelay, aType, aCallback) {
+ if (aCallback)
+ this._callback = aCallback;
+ this.clear();
+ this._timer.initWithCallback(this, aDelay, aType);
+ this._type = aType;
+ return this;
+ },
+
+ // Do the callback once. Cancels other timeouts on this object.
+ once: function once(aDelay, aCallback) {
+ return this._start(aDelay, this._timer.TYPE_ONE_SHOT, aCallback);
+ },
+
+ // Do the callback every aDelay msecs. Cancels other timeouts on this object.
+ interval: function interval(aDelay, aCallback) {
+ return this._start(aDelay, this._timer.TYPE_REPEATING_SLACK, aCallback);
+ },
+
+ // Clear any pending timeouts.
+ clear: function clear() {
+ if (this.isPending()) {
+ this._timer.cancel();
+ this._type = null;
+ }
+ return this;
+ },
+
+ // If there is a pending timeout, call it and cancel the timeout.
+ flush: function flush() {
+ if (this.isPending()) {
+ this.notify();
+ this.clear();
+ }
+ return this;
+ },
+
+ // Return true if we are waiting for a callback.
+ isPending: function isPending() {
+ return this._type !== null;
+ }
+};
+
diff --git a/browser/metro/base/content/WebProgress.js b/browser/metro/base/content/WebProgress.js
new file mode 100644
index 000000000000..5ad86bae344c
--- /dev/null
+++ b/browser/metro/base/content/WebProgress.js
@@ -0,0 +1,209 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Progress heartbeat timer duration (ms)
+const kHeartbeatDuration = 1000;
+// Start and end progress screen css margins as percentages
+const kProgressMarginStart = 30;
+const kProgressMarginEnd = 70;
+
+const WebProgress = {
+ _progressActive: false,
+
+ init: function init() {
+ messageManager.addMessageListener("Content:StateChange", this);
+ messageManager.addMessageListener("Content:LocationChange", this);
+ messageManager.addMessageListener("Content:SecurityChange", this);
+ Elements.progress.addEventListener("transitionend", this._progressTransEnd, true);
+ return this;
+ },
+
+ receiveMessage: function receiveMessage(aMessage) {
+ let json = aMessage.json;
+ let tab = Browser.getTabForBrowser(aMessage.target);
+
+ switch (aMessage.name) {
+ case "Content:StateChange": {
+ if (json.stateFlags & Ci.nsIWebProgressListener.STATE_IS_WINDOW) {
+ if (json.stateFlags & Ci.nsIWebProgressListener.STATE_START)
+ this._windowStart(json, tab);
+ else if (json.stateFlags & Ci.nsIWebProgressListener.STATE_STOP)
+ this._windowStop(json, tab);
+ }
+
+ if (json.stateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK) {
+ if (json.stateFlags & Ci.nsIWebProgressListener.STATE_START)
+ this._networkStart(json, tab);
+ else if (json.stateFlags & Ci.nsIWebProgressListener.STATE_STOP)
+ this._networkStop(json, tab);
+ }
+
+ this._progressStep();
+ break;
+ }
+
+ case "Content:LocationChange": {
+ this._locationChange(json, tab);
+ this._progressStep();
+ break;
+ }
+
+ case "Content:SecurityChange": {
+ this._securityChange(json, tab);
+ this._progressStep();
+ break;
+ }
+ }
+ },
+
+ _securityChange: function _securityChange(aJson, aTab) {
+ // Don't need to do anything if the data we use to update the UI hasn't changed
+ if (aTab.state == aJson.state && !aTab.hostChanged)
+ return;
+
+ aTab.hostChanged = false;
+ aTab.state = aJson.state;
+
+ if (aTab == Browser.selectedTab) {
+ IdentityUI.checkIdentity();
+ }
+ },
+
+ _locationChange: function _locationChange(aJson, aTab) {
+ let spec = aJson.location;
+ let location = spec.split("#")[0]; // Ignore fragment identifier changes.
+
+ if (aTab == Browser.selectedTab)
+ BrowserUI.updateURI();
+
+ let locationHasChanged = (location != aTab.browser.lastLocation);
+ if (locationHasChanged) {
+ Browser.getNotificationBox(aTab.browser).removeTransientNotifications();
+ aTab.resetZoomLevel();
+ aTab.hostChanged = true;
+ aTab.browser.lastLocation = location;
+ aTab.browser.userTypedValue = "";
+ aTab.browser.appIcon = { href: null, size:-1 };
+
+#ifdef MOZ_CRASHREPORTER
+ if (CrashReporter.enabled)
+ CrashReporter.annotateCrashReport("URL", spec);
+#endif
+ this._waitForLoad(aTab);
+ }
+
+ let event = document.createEvent("UIEvents");
+ event.initUIEvent("URLChanged", true, false, window, locationHasChanged);
+ aTab.browser.dispatchEvent(event);
+ },
+
+ _waitForLoad: function _waitForLoad(aTab) {
+ let browser = aTab.browser;
+
+ aTab._firstPaint = false;
+
+ browser.messageManager.addMessageListener("Browser:FirstPaint", function firstPaintListener(aMessage) {
+ browser.messageManager.removeMessageListener(aMessage.name, arguments.callee);
+ aTab._firstPaint = true;
+ aTab.scrolledAreaChanged(true);
+ aTab.updateThumbnailSource();
+ });
+ },
+
+ _networkStart: function _networkStart(aJson, aTab) {
+ aTab.startLoading();
+
+ if (aTab == Browser.selectedTab) {
+ BrowserUI.update(TOOLBARSTATE_LOADING);
+
+ // We should at least show something in the URLBar until
+ // the load has progressed further along
+ if (aTab.browser.currentURI.spec == "about:blank")
+ BrowserUI.updateURI({ captionOnly: true });
+ }
+ },
+
+ _networkStop: function _networkStop(aJson, aTab) {
+ aTab.endLoading();
+
+ if (aTab == Browser.selectedTab) {
+ BrowserUI.update(TOOLBARSTATE_LOADED);
+ }
+ },
+
+ _windowStart: function _windowStart(aJson, aTab) {
+ this._progressStart(aJson, aTab);
+ },
+
+ _windowStop: function _windowStop(aJson, aTab) {
+ this._progressStop(aJson, aTab);
+ },
+
+ _progressStart: function _progressStart(aJson, aTab) {
+ // We will get multiple calls from _windowStart, so
+ // only process once.
+ if (this._progressActive)
+ return;
+
+ this._progressActive = true;
+
+ // 'Whoosh' in
+ this._progressCount = kProgressMarginStart;
+ Elements.progress.style.width = this._progressCount + "%";
+ Elements.progress.removeAttribute("fade");
+
+ // Create a pulse timer to keep things moving even if we don't
+ // collect any state changes.
+ setTimeout(function() {
+ WebProgress._progressStepTimer();
+ }, kHeartbeatDuration, this);
+ },
+
+ _stepProgressCount: function _stepProgressCount() {
+ // Step toward the end margin in smaller slices as we get closer
+ let left = kProgressMarginEnd - this._progressCount;
+ let step = left * .05;
+ this._progressCount += Math.ceil(step);
+
+ // Don't go past the 'whoosh out' margin.
+ if (this._progressCount > kProgressMarginEnd) {
+ this._progressCount = kProgressMarginEnd;
+ }
+ },
+
+ _progressStep: function _progressStep() {
+ if (!this._progressActive)
+ return;
+ this._stepProgressCount();
+ Elements.progress.style.width = this._progressCount + "%";
+ },
+
+ _progressStepTimer: function _progressStepTimer() {
+ if (!this._progressActive)
+ return;
+ this._progressStep();
+
+ setTimeout(function() {
+ WebProgress._progressStepTimer();
+ }, kHeartbeatDuration, this);
+ },
+
+ _progressStop: function _progressStop(aJson, aTab) {
+ this._progressActive = false;
+ // 'Whoosh out' and fade
+ Elements.progress.style.width = "100%";
+ Elements.progress.setAttribute("fade", true);
+ },
+
+ _progressTransEnd: function _progressTransEnd(data) {
+ if (!Elements.progress.hasAttribute("fade"))
+ return;
+ // Close out fade finished, reset
+ if (data.propertyName == "opacity") {
+ Elements.progress.style.width = "0px";
+ }
+ },
+};
+
+
diff --git a/browser/metro/base/content/appbar.js b/browser/metro/base/content/appbar.js
new file mode 100644
index 000000000000..ca948a615aae
--- /dev/null
+++ b/browser/metro/base/content/appbar.js
@@ -0,0 +1,230 @@
+var Appbar = {
+ get appbar() { return document.getElementById('appbar'); },
+ get consoleButton() { return document.getElementById('console-button'); },
+ get jsShellButton() { return document.getElementById('jsshell-button'); },
+ get zoomInButton() { return document.getElementById('zoomin-button'); },
+ get zoomOutButton() { return document.getElementById('zoomout-button'); },
+ get starButton() { return document.getElementById('star-button'); },
+ get pinButton() { return document.getElementById('pin-button'); },
+ get moreButton() { return document.getElementById('more-button'); },
+
+ // track selected/active richgrid/tilegroup - the context for contextual action buttons
+ activeTileset: null,
+
+ init: function Appbar_init() {
+ window.addEventListener('MozContextUIShow', this, false);
+ window.addEventListener('MozPrecisePointer', this, false);
+ window.addEventListener('MozImprecisePointer', this, false);
+
+ this._updateDebugButtons();
+ this._updateZoomButtons();
+
+ // tilegroup selection events for all modules get bubbled up
+ window.addEventListener("selectionchange", this, false);
+ },
+
+ handleEvent: function Appbar_handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case 'MozContextUIShow':
+ this._updatePinButton();
+ this._updateStarButton();
+ break;
+ case 'MozPrecisePointer':
+ case 'MozImprecisePointer':
+ this._updateZoomButtons();
+ break;
+ case "selectionchange":
+ let nodeName = aEvent.target.nodeName;
+ if ('richgrid' === nodeName) {
+ this._onTileSelectionChanged(aEvent);
+ }
+ break;
+ }
+ },
+
+ onDownloadButton: function() {
+ PanelUI.show("downloads-container");
+ ContextUI.dismiss();
+ },
+
+ onZoomOutButton: function() {
+ Browser.zoom(1);
+ },
+
+ onZoomInButton: function() {
+ Browser.zoom(-1);
+ },
+
+ onPinButton: function() {
+ if (this.pinButton.checked) {
+ Browser.pinSite();
+ } else {
+ Browser.unpinSite();
+ }
+ },
+
+ onStarButton: function(aValue) {
+ if (aValue === undefined) {
+ aValue = this.starButton.checked;
+ }
+
+ if (aValue) {
+ Browser.starSite(function () {
+ Appbar._updateStarButton();
+ });
+ } else {
+ Browser.unstarSite(function () {
+ Appbar._updateStarButton();
+ });
+ }
+ },
+
+ onMoreButton: function(aEvent) {
+ var typesArray = ["find-in-page"];
+ try {
+ // If we have a valid http or https URI then show the view on desktop
+ // menu item.
+ var uri = Services.io.newURI(Browser.selectedBrowser.currentURI.spec,
+ null, null);
+ if (uri.schemeIs('http') || uri.schemeIs('https')) {
+ typesArray.push("view-on-desktop");
+ }
+ } catch(ex) {
+ }
+
+ var x = this.moreButton.getBoundingClientRect().left;
+ var y = this.appbar.getBoundingClientRect().top;
+ ContextMenuUI.showContextMenu({
+ json: {
+ types: typesArray,
+ string: '',
+ xPos: x,
+ yPos: y,
+ forcePosition: true,
+ leftAligned: true,
+ bottomAligned: true
+ }
+
+ });
+ },
+
+ onViewOnDesktop: function() {
+ try {
+ // Make sure we have a valid URI so Windows doesn't prompt
+ // with an unrecognized command, select default program window
+ var uri = Services.io.newURI(Browser.selectedBrowser.currentURI.spec,
+ null, null);
+ if (uri.schemeIs('http') || uri.schemeIs('https')) {
+ MetroUtils.launchInDesktop(Browser.selectedBrowser.currentURI.spec, "");
+ }
+ } catch(ex) {
+ }
+ },
+
+ onConsoleButton: function() {
+ PanelUI.show("console-container");
+ },
+
+ onJSShellButton: function() {
+ // XXX for debugging, this only works when running on the desktop.
+ if (!MetroUtils.immersive)
+ window.openDialog("chrome://browser/content/shell.xul", "_blank",
+ "all=no,scrollbars=yes,resizable=yes,dialog=no");
+ },
+
+ dispatchContextualAction: function(aActionName){
+ let activeTileset = this.activeTileset;
+ if (activeTileset) {
+ // fire event on the richgrid, others can listen
+ // but we keep coupling loose so grid doesn't need to know about appbar
+ let event = document.createEvent("Events");
+ event.action = aActionName;
+ event.initEvent("context-action", true, false);
+ activeTileset.dispatchEvent(event);
+
+ // done with this selection, explicitly clear it
+ activeTileset.clearSelection();
+ }
+ this.appbar.dismiss();
+ },
+
+ showContextualActions: function(aVerbs){
+ let doc = document;
+
+ // button element id to action verb lookup
+ let buttonsMap = new Map();
+ for (let verb of aVerbs) {
+ let id = verb + "-selected-button";
+ let buttonNode = doc.getElementById(id);
+ if (buttonNode) {
+ buttonsMap.set(id, verb);
+ } else {
+ Util.dumpLn("Appbar.showContextualActions: no button for " + verb);
+ }
+ }
+
+ // hide/show buttons as appropriate
+ let buttons = doc.querySelectorAll("#contextualactions-tray > toolbarbutton");
+
+ for (let btnNode of buttons) {
+ if (buttonsMap.has(btnNode.id)){
+ btnNode.hidden = false;
+ } else {
+ btnNode.hidden = true;
+ }
+ };
+
+ if (buttonsMap.size) {
+ // there are buttons to show
+ // TODO: show the contextual actions tray?
+ } else {
+ // 0 actions to show;
+ // TODO: hide the contextual actions tray entirely?
+ }
+ },
+
+ _onTileSelectionChanged: function _onTileSelectionChanged(aEvent){
+ let activeTileset = aEvent.target;
+
+ // deselect tiles in other tile groups
+ if (this.activeTileset && this.activeTileset !== activeTileset) {
+ this.activeTileset.clearSelection();
+ }
+ // keep track of which view is the target/scope for the contextual actions
+ this.activeTileset = activeTileset;
+
+ // ask the view for the list verbs/action-names it thinks are
+ // appropriate for the tiles selected
+ let contextActions = activeTileset.contextActions;
+ let verbs = [v for (v of contextActions)];
+
+ // could transition in old, new buttons?
+ this.showContextualActions(verbs);
+
+ if (verbs.length) {
+ this.appbar.show();
+ } else {
+ this.appbar.dismiss();
+ }
+ },
+
+ _updatePinButton: function() {
+ this.pinButton.checked = Browser.isSitePinned();
+ },
+
+ _updateStarButton: function() {
+ Browser.isSiteStarredAsync(function (isStarred) {
+ this.starButton.checked = isStarred;
+ }.bind(this));
+ },
+
+ _updateDebugButtons: function() {
+ this.consoleButton.disabled = !ConsolePanelView.enabled;
+ this.jsShellButton.disabled = MetroUtils.immersive;
+ },
+
+ _updateZoomButtons: function() {
+ let zoomDisabled = !InputSourceHelper.isPrecise;
+ this.zoomOutButton.disabled = this.zoomInButton.disabled = zoomDisabled;
+ }
+ };
diff --git a/browser/metro/base/content/bindings/appbar.xml b/browser/metro/base/content/bindings/appbar.xml
new file mode 100644
index 000000000000..8f8df361d99d
--- /dev/null
+++ b/browser/metro/base/content/bindings/appbar.xml
@@ -0,0 +1,89 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false
+ document.getAnonymousElementByAttribute(this, "anonid", "toolbar");
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false;
+
+
+
diff --git a/browser/metro/base/content/bindings/arrowbox.xml b/browser/metro/base/content/bindings/arrowbox.xml
new file mode 100644
index 000000000000..1f1f0e6262ee
--- /dev/null
+++ b/browser/metro/base/content/bindings/arrowbox.xml
@@ -0,0 +1,274 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ window.innerHeight) {
+ content.style.overflow = "hidden";
+ this.style.minHeight = (window.innerHeight - parseInt(this.top) - kBottomMargin) + "px";
+ }
+
+ let HALF_ARROW_WIDTH = 16;
+
+ let anchorClass = "";
+ let hideArrow = false;
+ if (horizPos == 0) {
+ container.orient = "vertical";
+ arrowbox.orient = "";
+ if (vertPos == 0) {
+ hideArrow = true;
+ } else {
+ let anchorPosX = 0.5;
+ // check for hasAttribute because, in some cases, anchorNode is actually a rect
+ if (this.anchorNode && this.anchorNode.hasAttribute && this.anchorNode.hasAttribute("anchorPosX"))
+ anchorPosX = parseFloat(this.anchorNode.getAttribute("anchorPosX")) || 0.5;
+ arrowbox.style.marginLeft = ((targetRect.left - popupRect.left) + (targetRect.width * anchorPosX) - HALF_ARROW_WIDTH) + "px";
+ if (vertPos == 1) {
+ container.dir = "normal";
+ anchorClass = "top";
+ } else if (vertPos == -1) {
+ container.dir = "reverse";
+ anchorClass = "bottom";
+ }
+ }
+ } else if (vertPos == 0) {
+ container.orient = "";
+ arrowbox.orient = "vertical";
+ let anchorPosY = 0.5;
+ // check for hasAttribute because, in some cases, anchorNode is actually a rect
+ if (this.anchorNode && this.anchorNode.hasAttribute && this.anchorNode.hasAttribute("anchorPosY"))
+ anchorPosY = parseFloat(this.anchorNode.getAttribute("anchorPosY")) || 0.5;
+ arrowbox.style.marginTop = ((targetRect.top - popupRect.top) + (targetRect.height * anchorPosY) - HALF_ARROW_WIDTH) + "px";
+ if (horizPos == 1) {
+ container.dir = "ltr";
+ anchorClass = "left";
+ } else if (horizPos == -1) {
+ container.dir = "rtl";
+ anchorClass = "right";
+ }
+ } else {
+ hideArrow = true;
+ }
+ arrow.hidden = hideArrow;
+ arrow.setAttribute("side", anchorClass);
+ ]]>
+
+
+ null
+
+
+
+
+ window.innerWidth)
+ left = window.innerWidth - popupRect.width;
+ else if (left < 0)
+ left = 1;
+
+ popupRect.left = left;
+ this.setAttribute("left", left);
+ popupRect.top = top;
+ this.setAttribute("top", top);
+ } else {
+ horizPos = (Math.round(popupRect.right) <= Math.round(anchorRect.left + offset)) ? -1 :
+ (Math.round(popupRect.left) >= Math.round(anchorRect.right - offset)) ? 1 : 0;
+ vertPos = (Math.round(popupRect.bottom) <= Math.round(anchorRect.top + offset)) ? -1 :
+ (Math.round(popupRect.top) >= Math.round(anchorRect.bottom - offset)) ? 1 : 0;
+ }
+ this._updateArrow(popupRect, anchorRect, horizPos, vertPos);
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/metro/base/content/bindings/autocomplete.xml b/browser/metro/base/content/bindings/autocomplete.xml
new file mode 100644
index 000000000000..dbe2dfe6e7a8
--- /dev/null
+++ b/browser/metro/base/content/bindings/autocomplete.xml
@@ -0,0 +1,445 @@
+
+
+
+
+
+%browserDTD;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/metro/base/content/bindings/bindings.xml b/browser/metro/base/content/bindings/bindings.xml
new file mode 100644
index 000000000000..a2ba5420243e
--- /dev/null
+++ b/browser/metro/base/content/bindings/bindings.xml
@@ -0,0 +1,407 @@
+
+
+
+
+
+%browserDTD;
+]>
+
+
+
+
+
+
+
+
+ 0.8)
+ this._insertItems();
+ ]]>
+
+
+
+
+
+
+ this.scrollBoxObject.height;
+ []
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ document.getAnonymousElementByAttribute(this, "anonid", "previous-button");
+
+
+
+ document.getAnonymousElementByAttribute(this, "anonid", "next-button");
+
+
+
+ document.getAnonymousElementByAttribute(this, "anonid", "close-button");
+
+
+
+
+
+
+
+
+ null
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0 || selectionEnd < aTextbox.textLength)
+ json.types.push("select-all");
+
+ let clipboard = Components.classes["@mozilla.org/widget/clipboard;1"].
+ getService(Ci.nsIClipboard);
+ let flavors = ["text/unicode"];
+ let hasData = clipboard.hasDataMatchingFlavors(flavors, flavors.length, Ci.nsIClipboard.kGlobalClipboard);
+
+ if (hasData && (!aTextbox.readOnly || aIgnoreReadOnly)) {
+ json.types.push("paste");
+ if (aTextbox.type == "url") {
+ json.types.push("paste-url");
+ }
+ }
+ json.xPos = aEvent.clientX;
+ json.yPos = aEvent.clientY;
+ json.source = aEvent.mozInputSource;
+ ContextMenuUI.showContextMenu({ target: aTextbox, json: json });
+ ]]>
+
+
+
+
diff --git a/browser/metro/base/content/bindings/browser.js b/browser/metro/base/content/bindings/browser.js
new file mode 100644
index 000000000000..e011b6f40841
--- /dev/null
+++ b/browser/metro/base/content/bindings/browser.js
@@ -0,0 +1,796 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+let Cc = Components.classes;
+let Ci = Components.interfaces;
+let Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+let WebProgressListener = {
+ _lastLocation: null,
+ _firstPaint: false,
+
+ init: function() {
+ let flags = Ci.nsIWebProgress.NOTIFY_LOCATION |
+ Ci.nsIWebProgress.NOTIFY_SECURITY |
+ Ci.nsIWebProgress.NOTIFY_STATE_WINDOW |
+ Ci.nsIWebProgress.NOTIFY_STATE_NETWORK |
+ Ci.nsIWebProgress.NOTIFY_STATE_DOCUMENT;
+
+ let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebProgress);
+ webProgress.addProgressListener(this, flags);
+ },
+
+ onStateChange: function onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
+ if (content != aWebProgress.DOMWindow)
+ return;
+
+ sendAsyncMessage("Content:StateChange", {
+ contentWindowId: this.contentWindowId,
+ stateFlags: aStateFlags,
+ status: aStatus
+ });
+ },
+
+ onLocationChange: function onLocationChange(aWebProgress, aRequest, aLocationURI, aFlags) {
+ if (content != aWebProgress.DOMWindow)
+ return;
+
+ let spec = aLocationURI ? aLocationURI.spec : "";
+ let location = spec.split("#")[0];
+
+ let charset = content.document.characterSet;
+
+ sendAsyncMessage("Content:LocationChange", {
+ contentWindowId: this.contentWindowId,
+ documentURI: aWebProgress.DOMWindow.document.documentURIObject.spec,
+ location: spec,
+ canGoBack: docShell.canGoBack,
+ canGoForward: docShell.canGoForward,
+ charset: charset.toString()
+ });
+
+ this._firstPaint = false;
+ let self = this;
+
+ // Keep track of hash changes
+ this.hashChanged = (location == this._lastLocation);
+ this._lastLocation = location;
+
+ // When a new page is loaded fire a message for the first paint
+ addEventListener("MozAfterPaint", function(aEvent) {
+ removeEventListener("MozAfterPaint", arguments.callee, true);
+
+ self._firstPaint = true;
+ let scrollOffset = ContentScroll.getScrollOffset(content);
+ sendAsyncMessage("Browser:FirstPaint", scrollOffset);
+ }, true);
+ },
+
+ onStatusChange: function onStatusChange(aWebProgress, aRequest, aStatus, aMessage) {
+ },
+
+ onSecurityChange: function onSecurityChange(aWebProgress, aRequest, aState) {
+ if (content != aWebProgress.DOMWindow)
+ return;
+
+ let serialization = SecurityUI.getSSLStatusAsString();
+
+ sendAsyncMessage("Content:SecurityChange", {
+ contentWindowId: this.contentWindowId,
+ SSLStatusAsString: serialization,
+ state: aState
+ });
+ },
+
+ get contentWindowId() {
+ return content.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils)
+ .currentInnerWindowID;
+ },
+
+ QueryInterface: function QueryInterface(aIID) {
+ if (aIID.equals(Ci.nsIWebProgressListener) ||
+ aIID.equals(Ci.nsISupportsWeakReference) ||
+ aIID.equals(Ci.nsISupports)) {
+ return this;
+ }
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+};
+
+WebProgressListener.init();
+
+
+let SecurityUI = {
+ getSSLStatusAsString: function() {
+ let status = docShell.securityUI.QueryInterface(Ci.nsISSLStatusProvider).SSLStatus;
+
+ if (status) {
+ let serhelper = Cc["@mozilla.org/network/serialization-helper;1"]
+ .getService(Ci.nsISerializationHelper);
+
+ status.QueryInterface(Ci.nsISerializable);
+ return serhelper.serializeToString(status);
+ }
+
+ return null;
+ }
+};
+
+let WebNavigation = {
+ _webNavigation: docShell.QueryInterface(Ci.nsIWebNavigation),
+ _timer: null,
+
+ init: function() {
+ addMessageListener("WebNavigation:GoBack", this);
+ addMessageListener("WebNavigation:GoForward", this);
+ addMessageListener("WebNavigation:GotoIndex", this);
+ addMessageListener("WebNavigation:LoadURI", this);
+ addMessageListener("WebNavigation:Reload", this);
+ addMessageListener("WebNavigation:Stop", this);
+ },
+
+ receiveMessage: function(message) {
+ switch (message.name) {
+ case "WebNavigation:GoBack":
+ this.goBack();
+ break;
+ case "WebNavigation:GoForward":
+ this.goForward();
+ break;
+ case "WebNavigation:GotoIndex":
+ this.gotoIndex(message);
+ break;
+ case "WebNavigation:LoadURI":
+ this.loadURI(message);
+ break;
+ case "WebNavigation:Reload":
+ this.reload(message);
+ break;
+ case "WebNavigation:Stop":
+ this.stop(message);
+ break;
+ }
+ },
+
+ goBack: function() {
+ if (this._webNavigation.canGoBack)
+ this._webNavigation.goBack();
+ },
+
+ goForward: function() {
+ if (this._webNavigation.canGoForward)
+ this._webNavigation.goForward();
+ },
+
+ gotoIndex: function(message) {
+ this._webNavigation.gotoIndex(message.index);
+ },
+
+ loadURI: function(message) {
+ let flags = message.json.flags || this._webNavigation.LOAD_FLAGS_NONE;
+ this._webNavigation.loadURI(message.json.uri, flags, null, null, null);
+
+ let tabData = message.json;
+ if (tabData.entries) {
+ // We are going to load from history so kill the current load. We do not
+ // want the load added to the history anyway. We reload after resetting history
+ this._webNavigation.stop(this._webNavigation.STOP_ALL);
+ this._restoreHistory(tabData, 0);
+ }
+ },
+
+ reload: function(message) {
+ let flags = message.json.flags || this._webNavigation.LOAD_FLAGS_NONE;
+ this._webNavigation.reload(flags);
+ },
+
+ stop: function(message) {
+ let flags = message.json.flags || this._webNavigation.STOP_ALL;
+ this._webNavigation.stop(flags);
+ },
+
+ _restoreHistory: function _restoreHistory(aTabData, aCount) {
+ // We need to wait for the sessionHistory to be initialized and there
+ // is no good way to do this. We'll try a wait loop like desktop
+ try {
+ if (!this._webNavigation.sessionHistory)
+ throw new Error();
+ } catch (ex) {
+ if (aCount < 10) {
+ let self = this;
+ this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._timer.initWithCallback(function(aTimer) {
+ self._timer = null;
+ self._restoreHistory(aTabData, aCount + 1);
+ }, 100, Ci.nsITimer.TYPE_ONE_SHOT);
+ return;
+ }
+ }
+
+ let history = this._webNavigation.sessionHistory;
+ if (history.count > 0)
+ history.PurgeHistory(history.count);
+ history.QueryInterface(Ci.nsISHistoryInternal);
+
+ // helper hashes for ensuring unique frame IDs and unique document
+ // identifiers.
+ let idMap = { used: {} };
+ let docIdentMap = {};
+
+ for (let i = 0; i < aTabData.entries.length; i++) {
+ if (!aTabData.entries[i].url)
+ continue;
+ history.addEntry(this._deserializeHistoryEntry(aTabData.entries[i], idMap, docIdentMap), true);
+ }
+
+ // We need to force set the active history item and cause it to reload since
+ // we stop the load above
+ let activeIndex = (aTabData.index || aTabData.entries.length) - 1;
+ history.getEntryAtIndex(activeIndex, true);
+ history.QueryInterface(Ci.nsISHistory).reloadCurrentEntry();
+ },
+
+ _deserializeHistoryEntry: function _deserializeHistoryEntry(aEntry, aIdMap, aDocIdentMap) {
+ let shEntry = Cc["@mozilla.org/browser/session-history-entry;1"].createInstance(Ci.nsISHEntry);
+
+ shEntry.setURI(Services.io.newURI(aEntry.url, null, null));
+ shEntry.setTitle(aEntry.title || aEntry.url);
+ if (aEntry.subframe)
+ shEntry.setIsSubFrame(aEntry.subframe || false);
+ shEntry.loadType = Ci.nsIDocShellLoadInfo.loadHistory;
+ if (aEntry.contentType)
+ shEntry.contentType = aEntry.contentType;
+ if (aEntry.referrer)
+ shEntry.referrerURI = Services.io.newURI(aEntry.referrer, null, null);
+
+ if (aEntry.cacheKey) {
+ let cacheKey = Cc["@mozilla.org/supports-PRUint32;1"].createInstance(Ci.nsISupportsPRUint32);
+ cacheKey.data = aEntry.cacheKey;
+ shEntry.cacheKey = cacheKey;
+ }
+
+ if (aEntry.ID) {
+ // get a new unique ID for this frame (since the one from the last
+ // start might already be in use)
+ let id = aIdMap[aEntry.ID] || 0;
+ if (!id) {
+ for (id = Date.now(); id in aIdMap.used; id++);
+ aIdMap[aEntry.ID] = id;
+ aIdMap.used[id] = true;
+ }
+ shEntry.ID = id;
+ }
+
+ if (aEntry.docshellID)
+ shEntry.docshellID = aEntry.docshellID;
+
+ if (aEntry.structuredCloneState && aEntry.structuredCloneVersion) {
+ shEntry.stateData =
+ Cc["@mozilla.org/docshell/structured-clone-container;1"].
+ createInstance(Ci.nsIStructuredCloneContainer);
+
+ shEntry.stateData.initFromBase64(aEntry.structuredCloneState, aEntry.structuredCloneVersion);
+ }
+
+ if (aEntry.scroll) {
+ let scrollPos = aEntry.scroll.split(",");
+ scrollPos = [parseInt(scrollPos[0]) || 0, parseInt(scrollPos[1]) || 0];
+ shEntry.setScrollPosition(scrollPos[0], scrollPos[1]);
+ }
+
+ let childDocIdents = {};
+ if (aEntry.docIdentifier) {
+ // If we have a serialized document identifier, try to find an SHEntry
+ // which matches that doc identifier and adopt that SHEntry's
+ // BFCacheEntry. If we don't find a match, insert shEntry as the match
+ // for the document identifier.
+ let matchingEntry = aDocIdentMap[aEntry.docIdentifier];
+ if (!matchingEntry) {
+ matchingEntry = {shEntry: shEntry, childDocIdents: childDocIdents};
+ aDocIdentMap[aEntry.docIdentifier] = matchingEntry;
+ } else {
+ shEntry.adoptBFCacheEntry(matchingEntry);
+ childDocIdents = matchingEntry.childDocIdents;
+ }
+ }
+
+ if (aEntry.owner_b64) {
+ let ownerInput = Cc["@mozilla.org/io/string-input-stream;1"].createInstance(Ci.nsIStringInputStream);
+ let binaryData = atob(aEntry.owner_b64);
+ ownerInput.setData(binaryData, binaryData.length);
+ let binaryStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIObjectInputStream);
+ binaryStream.setInputStream(ownerInput);
+ try { // Catch possible deserialization exceptions
+ shEntry.owner = binaryStream.readObject(true);
+ } catch (ex) { dump(ex); }
+ }
+
+ if (aEntry.children && shEntry instanceof Ci.nsISHContainer) {
+ for (let i = 0; i < aEntry.children.length; i++) {
+ if (!aEntry.children[i].url)
+ continue;
+
+ // We're getting sessionrestore.js files with a cycle in the
+ // doc-identifier graph, likely due to bug 698656. (That is, we have
+ // an entry where doc identifier A is an ancestor of doc identifier B,
+ // and another entry where doc identifier B is an ancestor of A.)
+ //
+ // If we were to respect these doc identifiers, we'd create a cycle in
+ // the SHEntries themselves, which causes the docshell to loop forever
+ // when it looks for the root SHEntry.
+ //
+ // So as a hack to fix this, we restrict the scope of a doc identifier
+ // to be a node's siblings and cousins, and pass childDocIdents, not
+ // aDocIdents, to _deserializeHistoryEntry. That is, we say that two
+ // SHEntries with the same doc identifier have the same document iff
+ // they have the same parent or their parents have the same document.
+
+ shEntry.AddChild(this._deserializeHistoryEntry(aEntry.children[i], aIdMap, childDocIdents), i);
+ }
+ }
+
+ return shEntry;
+ },
+
+ sendHistory: function sendHistory() {
+ // We need to package up the session history and send it to the sessionstore
+ let entries = [];
+ let history = docShell.QueryInterface(Ci.nsIWebNavigation).sessionHistory;
+ for (let i = 0; i < history.count; i++) {
+ let entry = this._serializeHistoryEntry(history.getEntryAtIndex(i, false));
+ entries.push(entry);
+ }
+ let index = history.index + 1;
+ sendAsyncMessage("Content:SessionHistory", { entries: entries, index: index });
+ },
+
+ _serializeHistoryEntry: function _serializeHistoryEntry(aEntry) {
+ let entry = { url: aEntry.URI.spec };
+
+ if (aEntry.title && aEntry.title != entry.url)
+ entry.title = aEntry.title;
+
+ if (!(aEntry instanceof Ci.nsISHEntry))
+ return entry;
+
+ let cacheKey = aEntry.cacheKey;
+ if (cacheKey && cacheKey instanceof Ci.nsISupportsPRUint32 && cacheKey.data != 0)
+ entry.cacheKey = cacheKey.data;
+
+ entry.ID = aEntry.ID;
+ entry.docshellID = aEntry.docshellID;
+
+ if (aEntry.referrerURI)
+ entry.referrer = aEntry.referrerURI.spec;
+
+ if (aEntry.contentType)
+ entry.contentType = aEntry.contentType;
+
+ let x = {}, y = {};
+ aEntry.getScrollPosition(x, y);
+ if (x.value != 0 || y.value != 0)
+ entry.scroll = x.value + "," + y.value;
+
+ if (aEntry.owner) {
+ try {
+ let binaryStream = Cc["@mozilla.org/binaryoutputstream;1"].createInstance(Ci.nsIObjectOutputStream);
+ let pipe = Cc["@mozilla.org/pipe;1"].createInstance(Ci.nsIPipe);
+ pipe.init(false, false, 0, 0xffffffff, null);
+ binaryStream.setOutputStream(pipe.outputStream);
+ binaryStream.writeCompoundObject(aEntry.owner, Ci.nsISupports, true);
+ binaryStream.close();
+
+ // Now we want to read the data from the pipe's input end and encode it.
+ let scriptableStream = Cc["@mozilla.org/binaryinputstream;1"].createInstance(Ci.nsIBinaryInputStream);
+ scriptableStream.setInputStream(pipe.inputStream);
+ let ownerBytes = scriptableStream.readByteArray(scriptableStream.available());
+ // We can stop doing base64 encoding once our serialization into JSON
+ // is guaranteed to handle all chars in strings, including embedded
+ // nulls.
+ entry.owner_b64 = btoa(String.fromCharCode.apply(null, ownerBytes));
+ } catch (e) { dump(e); }
+ }
+
+ entry.docIdentifier = aEntry.BFCacheEntry.ID;
+
+ if (aEntry.stateData != null) {
+ entry.structuredCloneState = aEntry.stateData.getDataAsBase64();
+ entry.structuredCloneVersion = aEntry.stateData.formatVersion;
+ }
+
+ if (!(aEntry instanceof Ci.nsISHContainer))
+ return entry;
+
+ if (aEntry.childCount > 0) {
+ entry.children = [];
+ for (let i = 0; i < aEntry.childCount; i++) {
+ let child = aEntry.GetChildAt(i);
+ if (child)
+ entry.children.push(this._serializeHistoryEntry(child));
+ else // to maintain the correct frame order, insert a dummy entry
+ entry.children.push({ url: "about:blank" });
+
+ // don't try to restore framesets containing wyciwyg URLs (cf. bug 424689 and bug 450595)
+ if (/^wyciwyg:\/\//.test(entry.children[i].url)) {
+ delete entry.children;
+ break;
+ }
+ }
+ }
+
+ return entry;
+ }
+};
+
+WebNavigation.init();
+
+
+let DOMEvents = {
+ init: function() {
+ addEventListener("DOMContentLoaded", this, false);
+ addEventListener("DOMTitleChanged", this, false);
+ addEventListener("DOMLinkAdded", this, false);
+ addEventListener("DOMWillOpenModalDialog", this, false);
+ addEventListener("DOMModalDialogClosed", this, true);
+ addEventListener("DOMWindowClose", this, false);
+ addEventListener("DOMPopupBlocked", this, false);
+ addEventListener("pageshow", this, false);
+ addEventListener("pagehide", this, false);
+ },
+
+ handleEvent: function(aEvent) {
+ let document = content.document;
+ switch (aEvent.type) {
+ case "DOMContentLoaded":
+ if (document.documentURIObject.spec == "about:blank")
+ return;
+
+ sendAsyncMessage("DOMContentLoaded", { });
+
+ // Send the session history now too
+ WebNavigation.sendHistory();
+ break;
+
+ case "pageshow":
+ case "pagehide": {
+ if (aEvent.target.defaultView != content)
+ break;
+
+ let util = aEvent.target.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+
+ let json = {
+ contentWindowWidth: content.innerWidth,
+ contentWindowHeight: content.innerHeight,
+ windowId: util.outerWindowID,
+ persisted: aEvent.persisted
+ };
+
+ // Clear onload focus to prevent the VKB to be shown unexpectingly
+ // but only if the location has really changed and not only the
+ // fragment identifier
+ let contentWindowID = content.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
+ if (!WebProgressListener.hashChanged && contentWindowID == util.currentInnerWindowID) {
+ let focusManager = Cc["@mozilla.org/focus-manager;1"].getService(Ci.nsIFocusManager);
+ focusManager.clearFocus(content);
+ }
+
+ sendAsyncMessage(aEvent.type, json);
+ break;
+ }
+
+ case "DOMPopupBlocked": {
+ let util = aEvent.requestingWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ let json = {
+ windowId: util.outerWindowID,
+ popupWindowURI: {
+ spec: aEvent.popupWindowURI.spec,
+ charset: aEvent.popupWindowURI.originCharset
+ },
+ popupWindowFeatures: aEvent.popupWindowFeatures,
+ popupWindowName: aEvent.popupWindowName
+ };
+
+ sendAsyncMessage("DOMPopupBlocked", json);
+ break;
+ }
+
+ case "DOMTitleChanged":
+ sendAsyncMessage("DOMTitleChanged", { title: document.title });
+ break;
+
+ case "DOMLinkAdded":
+ let target = aEvent.originalTarget;
+ if (!target.href || target.disabled)
+ return;
+
+ let json = {
+ windowId: target.ownerDocument.defaultView.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID,
+ href: target.href,
+ charset: document.characterSet,
+ title: target.title,
+ rel: target.rel,
+ type: target.type
+ };
+
+ // rel=icon can also have a sizes attribute
+ if (target.hasAttribute("sizes"))
+ json.sizes = target.getAttribute("sizes");
+
+ sendAsyncMessage("DOMLinkAdded", json);
+ break;
+
+ case "DOMWillOpenModalDialog":
+ case "DOMModalDialogClosed":
+ case "DOMWindowClose":
+ let retvals = sendSyncMessage(aEvent.type, { });
+ for (let i in retvals) {
+ if (retvals[i].preventDefault) {
+ aEvent.preventDefault();
+ break;
+ }
+ }
+ break;
+ }
+ }
+};
+
+DOMEvents.init();
+
+let ContentScroll = {
+ _scrollOffset: { x: 0, y: 0 },
+
+ init: function() {
+ addMessageListener("Content:SetCacheViewport", this);
+ addMessageListener("Content:SetWindowSize", this);
+
+ addEventListener("scroll", this, false);
+ addEventListener("pagehide", this, false);
+ addEventListener("MozScrolledAreaChanged", this, false);
+ },
+
+ getScrollOffset: function(aWindow) {
+ let cwu = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ let scrollX = {}, scrollY = {};
+ cwu.getScrollXY(false, scrollX, scrollY);
+ return { x: scrollX.value, y: scrollY.value };
+ },
+
+ getScrollOffsetForElement: function(aElement) {
+ if (aElement.parentNode == aElement.ownerDocument)
+ return this.getScrollOffset(aElement.ownerDocument.defaultView);
+ return { x: aElement.scrollLeft, y: aElement.scrollTop };
+ },
+
+ setScrollOffsetForElement: function(aElement, aLeft, aTop) {
+ if (aElement.parentNode == aElement.ownerDocument) {
+ aElement.ownerDocument.defaultView.scrollTo(aLeft, aTop);
+ } else {
+ aElement.scrollLeft = aLeft;
+ aElement.scrollTop = aTop;
+ }
+ },
+
+ receiveMessage: function(aMessage) {
+ let json = aMessage.json;
+ switch (aMessage.name) {
+ case "Content:SetCacheViewport": {
+ // Set resolution for root view
+ let rootCwu = content.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ if (json.id == 1) {
+ rootCwu.setResolution(json.scale, json.scale);
+ if (!WebProgressListener._firstPaint)
+ break;
+ }
+
+ let displayport = new Rect(json.x, json.y, json.w, json.h);
+ if (displayport.isEmpty())
+ break;
+
+ // Map ID to element
+ let element = null;
+ try {
+ element = rootCwu.findElementWithViewId(json.id);
+ } catch(e) {
+ // This could give NS_ERROR_NOT_AVAILABLE. In that case, the
+ // presshell is not available because the page is reloading.
+ }
+
+ if (!element)
+ break;
+
+ let binding = element.ownerDocument.getBindingParent(element);
+ if (binding instanceof Ci.nsIDOMHTMLInputElement && binding.mozIsTextField(false))
+ break;
+
+ // Set the scroll offset for this element if specified
+ if (json.scrollX >= 0 && json.scrollY >= 0) {
+ this.setScrollOffsetForElement(element, json.scrollX, json.scrollY)
+ if (json.id == 1)
+ this._scrollOffset = this.getScrollOffset(content);
+ }
+
+ // Set displayport. We want to set this after setting the scroll offset, because
+ // it is calculated based on the scroll offset.
+ let scrollOffset = this.getScrollOffsetForElement(element);
+ let x = displayport.x - scrollOffset.x;
+ let y = displayport.y - scrollOffset.y;
+
+ if (json.id == 1) {
+ x = Math.round(x * json.scale) / json.scale;
+ y = Math.round(y * json.scale) / json.scale;
+ }
+
+ let win = element.ownerDocument.defaultView;
+ let winCwu = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ winCwu.setDisplayPortForElement(x, y, displayport.width, displayport.height, element);
+ break;
+ }
+
+ case "Content:SetWindowSize": {
+ let cwu = content.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ cwu.setCSSViewport(json.width, json.height);
+ break;
+ }
+ }
+ },
+
+ handleEvent: function(aEvent) {
+ switch (aEvent.type) {
+ case "pagehide":
+ this._scrollOffset = { x: 0, y: 0 };
+ break;
+
+ case "scroll": {
+ let doc = aEvent.target;
+ if (doc != content.document)
+ break;
+
+ this.sendScroll();
+ break;
+ }
+
+ case "MozScrolledAreaChanged": {
+ let doc = aEvent.originalTarget;
+ if (content != doc.defaultView) // We are only interested in root scroll pane changes
+ return;
+
+ sendAsyncMessage("MozScrolledAreaChanged", {
+ width: aEvent.width,
+ height: aEvent.height,
+ left: aEvent.x + content.scrollX
+ });
+
+ // Send event only after painting to make sure content views in the parent process have
+ // been updated.
+ addEventListener("MozAfterPaint", function afterPaint() {
+ removeEventListener("MozAfterPaint", afterPaint, false);
+ sendAsyncMessage("Content:UpdateDisplayPort");
+ }, false);
+
+ break;
+ }
+ }
+ },
+
+ sendScroll: function sendScroll() {
+ let scrollOffset = this.getScrollOffset(content);
+ if (this._scrollOffset.x == scrollOffset.x && this._scrollOffset.y == scrollOffset.y)
+ return;
+
+ this._scrollOffset = scrollOffset;
+ sendAsyncMessage("scroll", scrollOffset);
+ }
+};
+
+ContentScroll.init();
+
+let ContentActive = {
+ init: function() {
+ addMessageListener("Content:Activate", this);
+ addMessageListener("Content:Deactivate", this);
+ },
+
+ receiveMessage: function(aMessage) {
+ let json = aMessage.json;
+ switch (aMessage.name) {
+ case "Content:Deactivate":
+ docShell.isActive = false;
+ let cwu = content.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ if (json.keepviewport)
+ break;
+ cwu.setDisplayPortForElement(0, 0, 0, 0, content.document.documentElement);
+ break;
+
+ case "Content:Activate":
+ docShell.isActive = true;
+ break;
+ }
+ }
+};
+
+ContentActive.init();
+
+/**
+ * Helper class for IndexedDB, child part. Listens using
+ * the observer service for events regarding IndexedDB
+ * prompts, and sends messages to the parent to actually
+ * show the prompts.
+ */
+let IndexedDB = {
+ _permissionsPrompt: "indexedDB-permissions-prompt",
+ _permissionsResponse: "indexedDB-permissions-response",
+
+ _quotaPrompt: "indexedDB-quota-prompt",
+ _quotaResponse: "indexedDB-quota-response",
+ _quotaCancel: "indexedDB-quota-cancel",
+
+ waitingObservers: [],
+
+ init: function IndexedDBPromptHelper_init() {
+ let os = Services.obs;
+ os.addObserver(this, this._permissionsPrompt, false);
+ os.addObserver(this, this._quotaPrompt, false);
+ os.addObserver(this, this._quotaCancel, false);
+ addMessageListener("IndexedDB:Response", this);
+ },
+
+ observe: function IndexedDBPromptHelper_observe(aSubject, aTopic, aData) {
+ if (aTopic != this._permissionsPrompt && aTopic != this._quotaPrompt && aTopic != this._quotaCancel) {
+ throw new Error("Unexpected topic!");
+ }
+
+ let requestor = aSubject.QueryInterface(Ci.nsIInterfaceRequestor);
+ let observer = requestor.getInterface(Ci.nsIObserver);
+
+ let contentWindow = requestor.getInterface(Ci.nsIDOMWindow);
+ let contentDocument = contentWindow.document;
+
+ if (aTopic == this._quotaCancel) {
+ observer.observe(null, this._quotaResponse, Ci.nsIPermissionManager.UNKNOWN_ACTION);
+ return;
+ }
+
+ // Remote to parent
+ sendAsyncMessage("IndexedDB:Prompt", {
+ topic: aTopic,
+ host: contentDocument.documentURIObject.asciiHost,
+ location: contentDocument.location.toString(),
+ data: aData,
+ observerId: this.addWaitingObserver(observer)
+ });
+ },
+
+ receiveMessage: function(aMessage) {
+ let payload = aMessage.json;
+ switch (aMessage.name) {
+ case "IndexedDB:Response":
+ let observer = this.getAndRemoveWaitingObserver(payload.observerId);
+ observer.observe(null, payload.responseTopic, payload.permission);
+ }
+ },
+
+ addWaitingObserver: function(aObserver) {
+ let observerId = 0;
+ while (observerId in this.waitingObservers)
+ observerId++;
+ this.waitingObservers[observerId] = aObserver;
+ return observerId;
+ },
+
+ getAndRemoveWaitingObserver: function(aObserverId) {
+ let observer = this.waitingObservers[aObserverId];
+ delete this.waitingObservers[aObserverId];
+ return observer;
+ }
+};
+
+IndexedDB.init();
+
diff --git a/browser/metro/base/content/bindings/browser.xml b/browser/metro/base/content/bindings/browser.xml
new file mode 100644
index 000000000000..146c732aaf7a
--- /dev/null
+++ b/browser/metro/base/content/bindings/browser.xml
@@ -0,0 +1,1089 @@
+
+
+
+
+
+ %findBarDTD;
+]>
+
+
+
+
+
+ null
+
+
+
+
+
+ []
+
+
+ null
+
+
+ null
+
+
+
+ null
+
+
+ Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ null
+ null
+
+
+ 0
+ 0
+
+
+
+
+ 0
+ 0
+
+
+
+
+ 0
+
+
+
+
+
+ 1
+ 1
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ binding.
+ this.removeEventListener("pageshow", this.onPageShow, true);
+ this.removeEventListener("pagehide", this.onPageHide, true);
+ this.removeEventListener("DOMPopupBlocked", this.onPopupBlocked, true);
+ ]]>
+
+
+
+
+
+
+
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ null
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ true
+
+
+
+
+ ({})
+
+
+
+
+ = this.kDieTime)
+ this._die();
+ else
+ // This doesn't need to be exact, just be sure to clean up at some point.
+ this._timeout = setTimeout(this._dieIfOld.bind(this), this.kDieTime);
+ },
+
+ /** Cleanup after ourselves. */
+ _die: function() {
+ let timeout = this._timeout;
+ if (timeout) {
+ clearTimeout(timeout);
+ this._timeout = null;
+ }
+
+ if (this._contentView && Math.abs(this._pixelsPannedSinceRefresh) > 0)
+ this._updateCacheViewport();
+
+ // We expect contentViews to contain our ID. If not, something bad
+ // happened.
+ delete this.self._contentViews[this._id];
+ },
+
+ /**
+ * Given the cache size and the viewport size, this determines where the cache
+ * should start relative to the scroll position. This adjusts the position based
+ * on which direction the user is panning, so that we use our cache as
+ * effectively as possible.
+ *
+ * @param aDirection Negative means user is panning to the left or above
+ * Zero means user did not pan
+ * Positive means user is panning to the right or below
+ * @param aViewportSize The width or height of the viewport
+ * @param aCacheSize The width or height of the displayport
+ */
+ _getRelativeCacheStart: function(aDirection, aViewportSize, aCacheSize) {
+ // Remember that this is relative to the viewport scroll position.
+ // Let's assume we are thinking about the y-axis.
+ // The extreme cases:
+ // |0| would mean that there is no content available above
+ // |aViewportSize - aCacheSize| would mean no content available below
+ //
+ // Taking the average of the extremes puts equal amounts of cache on the
+ // top and bottom of the viewport. If we think of this like a weighted
+ // average, .5 is the sweet spot where equals amounts of content are
+ // above and below the visible area.
+ //
+ // This weight is therefore how much of the cache is above (or to the
+ // left) the visible area.
+ let cachedAbove = .5;
+
+ // If panning down, leave only 25% of the non-visible cache above.
+ if (aDirection > 0)
+ cachedAbove = .25;
+
+ // If panning up, Leave 75% of the non-visible cache above.
+ if (aDirection < 0)
+ cachedAbove = .75;
+
+ return (aViewportSize - aCacheSize) * cachedAbove;
+ },
+
+ /** Determine size of the pixel cache. */
+ _getCacheSize: function(viewportSize) {
+ let self = this.self;
+ let contentView = this._contentView;
+
+ let cacheWidth = self._cacheRatioWidth * viewportSize.width;
+ let cacheHeight = self._cacheRatioHeight * viewportSize.height;
+ let contentSize = this._getContentSize();
+ let contentWidth = contentSize.width;
+ let contentHeight = contentSize.height;
+
+ // There are common cases, such as long skinny pages, where our cache size is
+ // bigger than our content size. In those cases, we take that sliver of leftover
+ // space and apply it to the other dimension.
+ if (contentWidth < cacheWidth) {
+ cacheHeight += (cacheWidth - contentWidth) * cacheHeight / cacheWidth;
+ cacheWidth = contentWidth;
+ } else if (contentHeight < cacheHeight) {
+ cacheWidth += (cacheHeight - contentHeight) * cacheWidth / cacheHeight;
+ cacheHeight = contentHeight;
+ }
+
+ return { width: cacheWidth, height: cacheHeight };
+ },
+
+ _sendDisplayportUpdate: function(scrollX, scrollY) {
+ let self = this.self;
+ if (!self.active)
+ return;
+
+ let contentView = this._contentView;
+ let viewportSize = this._getViewportSize();
+ let cacheSize = this._getCacheSize(viewportSize);
+ let cacheX = this._getRelativeCacheStart(this._pixelsPannedSinceRefresh.x, viewportSize.width, cacheSize.width) + contentView.scrollX;
+ let cacheY = this._getRelativeCacheStart(this._pixelsPannedSinceRefresh.y, viewportSize.height, cacheSize.height) + contentView.scrollY;
+ let contentSize = this._getContentSize();
+
+ // Use our pixels efficiently and don't try to cache things outside of content
+ // boundaries (The left bound can be negative because of RTL).
+
+ let rootScale = self.scale;
+ let leftBound = self._contentDocumentLeft * rootScale;
+ let bounds = new Rect(leftBound, 0, contentSize.width, contentSize.height);
+ let displayport = new Rect(cacheX, cacheY, cacheSize.width, cacheSize.height);
+ displayport.translateInside(bounds);
+
+ self.messageManager.sendAsyncMessage("Content:SetCacheViewport", {
+ scrollX: Math.round(scrollX) / rootScale,
+ scrollY: Math.round(scrollY) / rootScale,
+ x: Math.round(displayport.x) / rootScale,
+ y: Math.round(displayport.y) / rootScale,
+ w: Math.round(displayport.width) / rootScale,
+ h: Math.round(displayport.height) / rootScale,
+ scale: rootScale,
+ id: contentView.id
+ });
+
+ this._pixelsPannedSinceRefresh.x = 0;
+ this._pixelsPannedSinceRefresh.y = 0;
+ },
+
+ _updateCSSViewport: function() {
+ let contentView = this._contentView;
+ this._sendDisplayportUpdate(contentView.scrollX,
+ contentView.scrollY);
+ },
+
+ /**
+ * The cache viewport is what parts of content is cached in the parent process for
+ * fast scrolling. This syncs that up with the current projection viewport.
+ */
+ _updateCacheViewport: function() {
+ // Do not update scroll values for content.
+ if (this.isRoot())
+ this._sendDisplayportUpdate(-1, -1);
+ else {
+ let contentView = this._contentView;
+ this._sendDisplayportUpdate(contentView.scrollX,
+ contentView.scrollY);
+ }
+ },
+
+ _getContentSize: function() {
+ let self = this.self;
+ return { width: this._contentView.contentWidth,
+ height: this._contentView.contentHeight };
+ },
+
+ _getViewportSize: function() {
+ let self = this.self;
+ if (this.isRoot()) {
+ let bcr = self.getBoundingClientRect();
+ return { width: bcr.width, height: bcr.height };
+ } else {
+ return { width: this._contentView.viewportWidth,
+ height: this._contentView.viewportHeight };
+ }
+ },
+
+ init: function(contentView) {
+ let self = this.self;
+
+ this._contentView = contentView;
+ this._id = contentView.id;
+ this._scale = 1;
+ self._contentViews[this._id] = this;
+
+ if (!this.isRoot()) {
+ // Non-root content views are short lived.
+ this._timeout = setTimeout(this._dieIfOld.bind(this), this.kDieTime);
+ // This iframe may not have a display port yet, so build up a cache
+ // immediately.
+ this._updateCacheViewport();
+ }
+ },
+
+ isRoot: function() {
+ return this.self._contentViewManager.rootContentView == this._contentView;
+ },
+
+ scrollBy: function(x, y) {
+ let self = this.self;
+
+ // Bounding content rectangle is in device pixels
+ let contentView = this._contentView;
+ let viewportSize = this._getViewportSize();
+ let contentSize = this._getContentSize();
+ // Calculate document dimensions in device pixels
+ let scrollRangeX = contentSize.width - viewportSize.width;
+ let scrollRangeY = contentSize.height - viewportSize.height;
+
+ let leftOffset = self._contentDocumentLeft * this._scale;
+ x = Math.floor(Math.max(leftOffset, Math.min(scrollRangeX + leftOffset, contentView.scrollX + x))) - contentView.scrollX;
+ y = Math.floor(Math.max(0, Math.min(scrollRangeY, contentView.scrollY + y))) - contentView.scrollY;
+
+ if (x == 0 && y == 0)
+ return;
+
+ contentView.scrollBy(x, y);
+
+ this._lastPanTime = Date.now();
+
+ this._pixelsPannedSinceRefresh.x += x;
+ this._pixelsPannedSinceRefresh.y += y;
+ if (Math.abs(this._pixelsPannedSinceRefresh.x) > 20 ||
+ Math.abs(this._pixelsPannedSinceRefresh.y) > 20)
+ this._updateCacheViewport();
+ },
+
+ scrollTo: function(x, y) {
+ let contentView = this._contentView;
+ this.scrollBy(x - contentView.scrollX, y - contentView.scrollY);
+ },
+
+ _setScale: function _setScale(scale) {
+ this._scale = scale;
+ this._contentView.setScale(scale, scale);
+ },
+
+ getPosition: function() {
+ let contentView = this._contentView;
+ return { x: contentView.scrollX, y: contentView.scrollY };
+ }
+ })
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/metro/base/content/bindings/console.xml b/browser/metro/base/content/bindings/console.xml
new file mode 100644
index 000000000000..627da33a1ed0
--- /dev/null
+++ b/browser/metro/base/content/bindings/console.xml
@@ -0,0 +1,61 @@
+
+
+
+
+
+%browserDTD;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/metro/base/content/bindings/dialog.xml b/browser/metro/base/content/bindings/dialog.xml
new file mode 100644
index 000000000000..dd1152744b52
--- /dev/null
+++ b/browser/metro/base/content/bindings/dialog.xml
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/metro/base/content/bindings/downloads.xml b/browser/metro/base/content/bindings/downloads.xml
new file mode 100644
index 000000000000..44ffed6075cb
--- /dev/null
+++ b/browser/metro/base/content/bindings/downloads.xml
@@ -0,0 +1,208 @@
+
+
+
+
+
+%browserDTD;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Components.interfaces.nsIDownloadManager
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/metro/base/content/bindings/flyoutpanel.xml b/browser/metro/base/content/bindings/flyoutpanel.xml
new file mode 100644
index 000000000000..b6770dda30dc
--- /dev/null
+++ b/browser/metro/base/content/bindings/flyoutpanel.xml
@@ -0,0 +1,95 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ false;
+
+
+
diff --git a/browser/metro/base/content/bindings/grid.xml b/browser/metro/base/content/bindings/grid.xml
new file mode 100644
index 000000000000..89b6a583096b
--- /dev/null
+++ b/browser/metro/base/content/bindings/grid.xml
@@ -0,0 +1,556 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ null
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ null
+
+
+
+
+
+
+
+
+
+
+ null
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ = 0) {
+ let selected = this.getItemAtIndex(val);
+ this.selectItem(selected);
+ } else {
+ this.clearSelection();
+ }
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ = lowerBound &&
+ this.selectedIndex < higherBound;
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 10
+
+
+
+
+
+ this._scheduledArrangeItemsTries) {
+ // schedule re-try of arrangeItems at the next tick
+ this._scheduledArrangeItemsTimerId = setTimeout(this.arrangeItems.bind(this), 0);
+ return;
+ }
+ }
+
+ // items ready to arrange (or retries max exceeded)
+ // reset the flags
+ if (this._scheduledArrangeItemsTimerId) {
+ clearTimeout(this._scheduledArrangeItemsTimerId);
+ delete this._scheduledArrangeItemsTimerId;
+ }
+ if (this._scheduledArrangeItemsTries) {
+ this._scheduledArrangeItemsTries = 0;
+ }
+
+ // Autocomplete is a binding within a binding, so we have to step
+ // up an additional parentNode.
+ let container = null;
+ if (this.parentNode.id == "results-vbox" ||
+ this.parentNode.id == "searches-vbox")
+ container = this.parentNode.parentNode.getBoundingClientRect();
+ else
+ container = this.parentNode.getBoundingClientRect();
+
+ // If we don't have valid dimensions we can't arrange yet
+ if (!container.height || !gridItemRect.height)
+ return;
+
+ // We favor overflowing horizontally, not vertically
+ let maxRowCount = Math.floor(container.height / gridItemRect.height) - 1;
+
+ if (aNumRows) {
+ this._rowCount = aNumRows;
+ } else {
+ this._rowCount = Math.min(this.itemCount, maxRowCount);
+ }
+ if (aNumColumns) {
+ this._columnCount = aNumColumns;
+ } else {
+ this._columnCount = Math.ceil(this.itemCount / this._rowCount);
+ }
+
+ this._grid.style.width = (this._columnCount * gridItemRect.width) + "px";
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ = 0 && anIndex < this.itemCount;
+ ]]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ null
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/metro/base/content/bindings/pageaction.xml b/browser/metro/base/content/bindings/pageaction.xml
new file mode 100644
index 000000000000..9492d595d743
--- /dev/null
+++ b/browser/metro/base/content/bindings/pageaction.xml
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/metro/base/content/bindings/selectionoverlay.xml b/browser/metro/base/content/bindings/selectionoverlay.xml
new file mode 100644
index 000000000000..0ed93b15b029
--- /dev/null
+++ b/browser/metro/base/content/bindings/selectionoverlay.xml
@@ -0,0 +1,126 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ document.getAnonymousElementByAttribute(this, "anonid", "selection-overlay-inner").parentNode;
+ document.getAnonymousElementByAttribute(this, "anonid", "selection-overlay-debug");
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/metro/base/content/bindings/tabs.xml b/browser/metro/base/content/bindings/tabs.xml
new file mode 100644
index 000000000000..d3ad1b113170
--- /dev/null
+++ b/browser/metro/base/content/bindings/tabs.xml
@@ -0,0 +1,173 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ document.getAnonymousElementByAttribute(this, "anonid", "thumbnail");
+ document.getAnonymousElementByAttribute(this, "anonid", "close");
+ document.getAnonymousElementByAttribute(this, "anonid", "title");
+ document.getAnonymousElementByAttribute(this, "anonid", "favicon");
+ this.parentNode;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ document.getAnonymousElementByAttribute(this, "anonid", "tabs-scrollbox");
+ null
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/metro/base/content/bindings/toggleswitch.xml b/browser/metro/base/content/bindings/toggleswitch.xml
new file mode 100644
index 000000000000..8dcc83355918
--- /dev/null
+++ b/browser/metro/base/content/bindings/toggleswitch.xml
@@ -0,0 +1,144 @@
+
+
+
+
+
+ %checkboxDTD;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ document.getAnonymousElementByAttribute(this, "anonid", "group");
+
+
+
+ document.getAnonymousElementByAttribute(this, "anonid", "on");
+
+
+
+ document.getAnonymousElementByAttribute(this, "anonid", "onlabel");
+
+
+
+ document.getAnonymousElementByAttribute(this, "anonid", "off");
+
+
+
+ document.getAnonymousElementByAttribute(this, "anonid", "offlabel");
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/metro/base/content/bookmarks.js b/browser/metro/base/content/bookmarks.js
new file mode 100644
index 000000000000..8e5dc4ca7aa5
--- /dev/null
+++ b/browser/metro/base/content/bookmarks.js
@@ -0,0 +1,364 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Utility singleton for manipulating bookmarks.
+ */
+var Bookmarks = {
+ get metroRoot() {
+ return PlacesUtils.annotations.getItemsWithAnnotation('metro/bookmarksRoot', {})[0];
+ },
+
+ logging: false,
+ log: function(msg) {
+ if (this.logging) {
+ Services.console.logStringMessage(msg);
+ }
+ },
+
+ /*
+ * fixupColorFormat - convert a decimal color value to a valid
+ * css color string of the format '#123456'
+ */
+ fixupColorFormat: function bv_fixupColorFormat(aColor) {
+ let color = aColor.toString(16);
+ while (color.length < 6)
+ color = "0" + color;
+ return "#" + color;
+ },
+
+ getFaveIconPrimaryColor: function bh_getFaveIconPrimaryColor(aBookmarkId) {
+ if (PlacesUtils.annotations.itemHasAnnotation(aBookmarkId, 'metro/faveIconColor'))
+ return PlacesUtils.annotations.getItemAnnotation(aBookmarkId, 'metro/faveIconColor');
+ return "";
+ },
+
+ setFaveIconPrimaryColor: function bh_setFaveIconPrimaryColor(aBookmarkId, aColorStr) {
+ var anno = [{ name: "metro/faveIconColor",
+ type: Ci.nsIAnnotationService.TYPE_STRING,
+ flags: 0,
+ value: aColorStr,
+ expires: Ci.nsIAnnotationService.EXPIRE_NEVER }];
+ PlacesUtils.setAnnotationsForItem(aBookmarkId, anno);
+ },
+
+ addForURI: function bh_addForURI(aURI, aTitle, callback) {
+ this.isURIBookmarked(aURI, function (isBookmarked) {
+ if (isBookmarked)
+ return;
+
+ let bookmarkTitle = aTitle || aURI.spec;
+ let bookmarkService = PlacesUtils.bookmarks;
+ let bookmarkId = bookmarkService.insertBookmark(Bookmarks.metroRoot,
+ aURI,
+ bookmarkService.DEFAULT_INDEX,
+ bookmarkTitle);
+
+ // XXX Used for browser-chrome tests
+ let event = document.createEvent("Events");
+ event.initEvent("BookmarkCreated", true, false);
+ window.dispatchEvent(event);
+
+ if (callback)
+ callback(bookmarkId);
+ });
+ },
+
+ isURIBookmarked: function bh_isURIBookmarked(aURI, callback) {
+ if (!callback)
+ return;
+ PlacesUtils.asyncGetBookmarkIds(aURI, function(aItemIds) {
+ callback(aItemIds && aItemIds.length > 0);
+ }, this);
+ },
+
+ removeForURI: function bh_removeForURI(aURI, callback) {
+ // XXX blargle xpconnect! might not matter, but a method on
+ // nsINavBookmarksService that takes an array of items to
+ // delete would be faster. better yet, a method that takes a URI!
+ PlacesUtils.asyncGetBookmarkIds(aURI, function(aItemIds) {
+ aItemIds.forEach(PlacesUtils.bookmarks.removeItem);
+ if (callback)
+ callback(aURI, aItemIds);
+
+ // XXX Used for browser-chrome tests
+ let event = document.createEvent("Events");
+ event.initEvent("BookmarkRemoved", true, false);
+ window.dispatchEvent(event);
+ });
+ }
+};
+
+/**
+ * Wraps a list/grid control implementing nsIDOMXULSelectControlElement and
+ * fills it with the user's bookmarks.
+ *
+ * @param aSet Control implementing nsIDOMXULSelectControlElement.
+ * @param {Number} aLimit Maximum number of items to show in the view.
+ * @param aRoot Bookmark root to show in the view.
+ */
+function BookmarksView(aSet, aLimit, aRoot) {
+ this._set = aSet;
+ this._set.controller = this;
+
+ this._limit = aLimit;
+
+ this._changes = new BookmarkChangeListener(this);
+ PlacesUtils.bookmarks.addObserver(this._changes, false);
+
+ // This also implicitly calls `getBookmarks`
+ this.root = aRoot;
+}
+
+BookmarksView.prototype = {
+ _limit: null,
+ _set: null,
+ _changes: null,
+ _root: null,
+ _sort: 0, // Natural bookmark order.
+
+ get sort() {
+ return this._sort;
+ },
+
+ set sort(aSort) {
+ this._sort = aSort;
+ this.getBookmarks();
+ },
+
+ get root() {
+ return this._root;
+ },
+
+ set root(aRoot) {
+ this._root = aRoot;
+ this.getBookmarks();
+ },
+
+ handleItemClick: function bv_handleItemClick(aItem) {
+ let url = aItem.getAttribute("value");
+ BrowserUI.goToURI(url);
+ },
+
+ _getItemForBookmarkId: function bv__getItemForBookmark(aBookmarkId) {
+ return this._set.querySelector("richgriditem[bookmarkId='" + aBookmarkId + "']");
+ },
+
+ _getBookmarkIdForItem: function bv__getBookmarkForItem(aItem) {
+ return aItem.getAttribute("bookmarkId");
+ },
+
+ _updateItemWithAttrs: function dv__updateItemWithAttrs(anItem, aAttrs) {
+ for (let name in aAttrs)
+ anItem.setAttribute(name, aAttrs[name]);
+ },
+
+ getBookmarks: function bv_getBookmarks() {
+ let options = gHistSvc.getNewQueryOptions();
+ options.queryType = options.QUERY_TYPE_BOOKMARKS;
+ options.excludeQueries = true; // Don't include "smart folders"
+ options.maxResults = this._limit;
+ options.sortingMode = this._sort;
+
+ let query = gHistSvc.getNewQuery();
+ query.setFolders([Bookmarks.metroRoot], 1);
+
+ let result = gHistSvc.executeQuery(query, options);
+ let rootNode = result.root;
+ rootNode.containerOpen = true;
+ let childCount = rootNode.childCount;
+
+ for (let i = 0; i < childCount; i++) {
+ let node = rootNode.getChild(i);
+
+ // Ignore folders, separators, undefined item types, etc.
+ if (node.type != node.RESULT_TYPE_URI &&
+ node.type != node.RESULT_TYPE_VISIT &&
+ node.type != node.RESULT_TYPE_FULL_VISIT)
+ continue;
+
+ this.addBookmark(node.itemId);
+ }
+
+ rootNode.containerOpen = false;
+ },
+
+ inCurrentView: function bv_inCurrentView(aParentId, aIndex, aItemType) {
+ if (this._root && aParentId != this._root)
+ return false;
+
+ if (this._limit && aIndex >= this._limit)
+ return false;
+
+ if (aItemType != PlacesUtils.bookmarks.TYPE_BOOKMARK)
+ return false;
+
+ return true;
+ },
+
+ clearBookmarks: function bv_clearBookmarks() {
+ while (this._set.itemCount > 0)
+ this._set.removeItemAt(0);
+ },
+
+ addBookmark: function bv_addBookmark(aBookmarkId) {
+ let bookmarks = PlacesUtils.bookmarks;
+
+ let index = bookmarks.getItemIndex(aBookmarkId);
+ let uri = bookmarks.getBookmarkURI(aBookmarkId);
+ let title = bookmarks.getItemTitle(aBookmarkId) || uri.spec;
+ let item = this._set.insertItemAt(index, title, uri.spec);
+ item.setAttribute("bookmarkId", aBookmarkId);
+ this._updateFavicon(aBookmarkId, item, uri);
+ },
+
+ _updateFavicon: function _updateFavicon(aBookmarkId, aItem, aUri) {
+ PlacesUtils.favicons.getFaviconURLForPage(aUri, this._gotIcon.bind(this, aBookmarkId, aItem));
+ },
+
+ _gotIcon: function _gotIcon(aBookmarkId, aItem, aIconUri) {
+ aItem.setAttribute("iconURI", aIconUri ? aIconUri.spec : "");
+
+ let color = Bookmarks.getFaveIconPrimaryColor(aBookmarkId);
+ if (color) {
+ aItem.color = color;
+ return;
+ }
+ let url = Services.io.newURI(aIconUri.spec.replace("moz-anno:favicon:",""), "", null)
+ let ca = Components.classes["@mozilla.org/places/colorAnalyzer;1"]
+ .getService(Components.interfaces.mozIColorAnalyzer);
+ ca.findRepresentativeColor(url, function (success, color) {
+ let colorStr = Bookmarks.fixupColorFormat(color);
+ Bookmarks.setFaveIconPrimaryColor(aBookmarkId, colorStr);
+ aItem.color = colorStr;
+ }, this);
+ },
+
+ updateBookmark: function bv_updateBookmark(aBookmarkId) {
+ let item = this._getItemForBookmarkId(aBookmarkId);
+
+ if (!item)
+ return;
+
+ let bookmarks = PlacesUtils.bookmarks;
+ let oldIndex = this._set.getIndexOfItem(item);
+ let index = bookmarks.getItemIndex(aBookmarkId);
+
+ if (oldIndex != index) {
+ this.removeBookmark(aBookmarkId);
+ this.addBookmark(aBookmarkId);
+ return;
+ }
+
+ let uri = bookmarks.getBookmarkURI(aBookmarkId);
+ let title = bookmarks.getItemTitle(aBookmarkId) || uri.spec;
+
+ item.setAttribute("value", uri.spec);
+ item.setAttribute("label", title);
+
+ this._updateFavicon(aBookmarkId, item, uri);
+ },
+
+ removeBookmark: function bv_removeBookmark(aBookmarkId) {
+ let item = this._getItemForBookmarkId(aBookmarkId);
+ let index = this._set.getIndexOfItem(item);
+ this._set.removeItemAt(index);
+ },
+
+ destruct: function bv_destruct() {
+ PlacesUtils.bookmarks.removeObserver(this._changes);
+ }
+};
+
+var BookmarksStartView = {
+ _view: null,
+ get _grid() { return document.getElementById("start-bookmarks-grid"); },
+
+ init: function init() {
+ this._view = new BookmarksView(this._grid, StartUI.maxResultsPerSection, Bookmarks.metroRoot);
+ },
+
+ uninit: function uninit() {
+ this._view.destruct();
+ },
+
+ show: function show() {
+ this._grid.arrangeItems();
+ }
+};
+
+var BookmarksPanelView = {
+ _view: null,
+
+ get _grid() { return document.getElementById("bookmarks-list"); },
+ get visible() { return PanelUI.isPaneVisible("bookmarks-container"); },
+
+ init: function init() {
+ this._view = new BookmarksView(this._grid, null, Bookmarks.metroRoot);
+ },
+
+ show: function show() {
+ this._grid.arrangeItems();
+ },
+
+ uninit: function uninit() {
+ this._view.destruct();
+ }
+};
+
+/**
+ * Observes bookmark changes and keeps a linked BookmarksView updated.
+ *
+ * @param aView An instance of BookmarksView.
+ */
+function BookmarkChangeListener(aView) {
+ this._view = aView;
+};
+
+BookmarkChangeListener.prototype = {
+ //////////////////////////////////////////////////////////////////////////////
+ //// nsINavBookmarkObserver
+ onBeginUpdateBatch: function () { },
+ onEndUpdateBatch: function () { },
+
+ onItemAdded: function bCL_onItemAdded(aItemId, aParentId, aIndex, aItemType, aURI, aTitle, aDateAdded, aGUID, aParentGUID) {
+ if (!this._view.inCurrentView(aParentId, aIndex, aItemType))
+ return;
+
+ this._view.addBookmark(aItemId);
+ },
+
+ onItemChanged: function bCL_onItemChanged(aItemId, aProperty, aIsAnnotationProperty, aNewValue, aLastModified, aItemType, aParentId, aGUID, aParentGUID) {
+ let itemIndex = PlacesUtils.bookmarks.getItemIndex(aItemId);
+ if (!this._view.inCurrentView(aParentId, itemIndex, aItemType))
+ return;
+
+ this._view.updateBookmark(aItemId);
+ },
+
+ onItemMoved: function bCL_onItemMoved(aItemId, aOldParentId, aOldIndex, aNewParentId, aNewIndex, aItemType, aGUID, aOldParentGUID, aNewParentGUID) {
+ let wasInView = this._view.inCurrentView(aOldParentId, aOldIndex, aItemType);
+ let nowInView = this._view.inCurrentView(aNewParentId, aNewIndex, aItemType);
+
+ if (!wasInView && nowInView)
+ this._view.addBookmark(aItemId, aParentId, aIndex, aItemType, aURI, aTitle, aDateAdded);
+
+ if (wasInView && !nowInView)
+ this._view.removeBookmark(aItemId);
+ },
+
+ onBeforeItemRemoved: function (aItemId, aItemType, aParentId, aGUID, aParentGUID) { },
+ onItemRemoved: function bCL_onItemRemoved(aItemId, aParentId, aIndex, aItemType, aURI, aGUID, aParentGUID) {
+ if (!this._view.inCurrentView(aParentId, aIndex, aItemType))
+ return;
+
+ this._view.removeBookmark(aItemId);
+ },
+
+ onItemVisited: function(aItemId, aVisitId, aTime, aTransitionType, aURI, aParentId, aGUID, aParentGUID) { },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// nsISupports
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsINavBookmarkObserver])
+};
diff --git a/browser/metro/base/content/browser-scripts.js b/browser/metro/base/content/browser-scripts.js
new file mode 100644
index 000000000000..97f9ae2afbc1
--- /dev/null
+++ b/browser/metro/base/content/browser-scripts.js
@@ -0,0 +1,170 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+/*
+ * JS modules
+ */
+
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
+ "resource://gre/modules/PluralForm.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils",
+ "resource://gre/modules/PlacesUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
+ "resource://gre/modules/NetUtil.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PdfJs",
+ "resource://pdf.js/PdfJs.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "DownloadUtils",
+ "resource://gre/modules/DownloadUtils.jsm");
+
+/*
+ * Services
+ */
+
+#ifdef XP_WIN
+XPCOMUtils.defineLazyServiceGetter(this, "MetroUtils",
+ "@mozilla.org/windows-metroutils;1",
+ "nsIWinMetroUtils");
+#else
+// Stub nsIWinMetroUtils implementation for testing on non-Windows platforms:
+var MetroUtils = {
+ snappedState: Ci.nsIWinMetroUtils.fullScreenLandscape,
+ immersive: false,
+ handPreference: Ci.nsIWinMetroUtils.handPreferenceLeft,
+ unsnap: function() {},
+ launchInDesktop: function() {},
+ pinTileAsync: function() {},
+ unpinTileAsync: function() {},
+ isTilePinned: function() { return false; },
+ keyboardVisible: false,
+ keyboardX: 0,
+ keyboardY: 0,
+ keyboardWidth: 0,
+ keyboardHeight: 0
+};
+#endif
+XPCOMUtils.defineLazyServiceGetter(this, "StyleSheetSvc",
+ "@mozilla.org/content/style-sheet-service;1",
+ "nsIStyleSheetService");
+XPCOMUtils.defineLazyServiceGetter(window, "gHistSvc",
+ "@mozilla.org/browser/nav-history-service;1",
+ "nsINavHistoryService",
+ "nsIBrowserHistory");
+XPCOMUtils.defineLazyServiceGetter(window, "gURIFixup",
+ "@mozilla.org/docshell/urifixup;1",
+ "nsIURIFixup");
+XPCOMUtils.defineLazyServiceGetter(window, "gFaviconService",
+ "@mozilla.org/browser/favicon-service;1",
+ "nsIFaviconService");
+XPCOMUtils.defineLazyServiceGetter(window, "gFocusManager",
+ "@mozilla.org/focus-manager;1",
+ "nsIFocusManager");
+#ifdef MOZ_CRASHREPORTER
+XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter",
+ "@mozilla.org/xre/app-info;1",
+ "nsICrashReporter");
+#endif
+
+/*
+ * window.Rect is used by
+ * http://www.w3.org/TR/DOM-Level-2-Style/css.html#CSS-Rect
+ * so it is not possible to set a lazy getter for Geometry.jsm.
+ */
+Cu.import("resource://gre/modules/Geometry.jsm");
+
+/*
+ * Browser scripts
+ */
+[
+ ["WebProgress", "chrome://browser/content/WebProgress.js"],
+ ["FindHelperUI", "chrome://browser/content/helperui/FindHelperUI.js"],
+ ["FormHelperUI", "chrome://browser/content/helperui/FormHelperUI.js"],
+ ["BrowserTouchHandler", "chrome://browser/content/BrowserTouchHandler.js"],
+ ["AlertsHelper", "chrome://browser/content/helperui/AlertsHelper.js"],
+ ["CapturePickerUI", "chrome://browser/content/helperui/CapturePickerUI.js"],
+ ["CharsetMenu", "chrome://browser/content/helperui/CharsetMenu.js"],
+ ["AutofillMenuUI", "chrome://browser/content/helperui/MenuUI.js"],
+ ["ContextMenuUI", "chrome://browser/content/helperui/MenuUI.js"],
+ ["MenuControlUI", "chrome://browser/content/helperui/MenuUI.js"],
+ ["MenuPopup", "chrome://browser/content/helperui/MenuUI.js"],
+ ["IdentityUI", "chrome://browser/content/helperui/IdentityUI.js"],
+ ["IndexedDB", "chrome://browser/content/helperui/IndexedDB.js"],
+ ["MasterPasswordUI", "chrome://browser/content/helperui/MasterPasswordUI.js"],
+ ["OfflineApps", "chrome://browser/content/helperui/OfflineApps.js"],
+ ["SelectHelperUI", "chrome://browser/content/helperui/SelectHelperUI.js"],
+ ["SelectionHelperUI", "chrome://browser/content/helperui/SelectionHelperUI.js"],
+ ["SharingUI", "chrome://browser/content/helperui/SharingUI.js"],
+ ["FullScreenVideo", "chrome://browser/content/video.js"],
+ ["AnimatedZoom", "chrome://browser/content/AnimatedZoom.js"],
+ ["CommandUpdater", "chrome://browser/content/commandUtil.js"],
+ ["ContextCommands", "chrome://browser/content/ContextCommands.js"],
+ ["Bookmarks", "chrome://browser/content/bookmarks.js"],
+ ["Downloads", "chrome://browser/content/downloads.js"],
+ ["BookmarksPanelView", "chrome://browser/content/bookmarks.js"],
+ ["ConsolePanelView", "chrome://browser/content/console.js"],
+ ["DownloadsPanelView", "chrome://browser/content/downloads.js"],
+ ["DownloadsView", "chrome://browser/content/downloads.js"],
+ ["Downloads", "chrome://browser/content/downloads.js"],
+ ["PreferencesPanelView", "chrome://browser/content/preferences.js"],
+ ["BookmarksStartView", "chrome://browser/content/bookmarks.js"],
+ ["HistoryView", "chrome://browser/content/history.js"],
+ ["HistoryStartView", "chrome://browser/content/history.js"],
+ ["HistoryPanelView", "chrome://browser/content/history.js"],
+ ["TopSitesView", "chrome://browser/content/TopSites.js"],
+ ["TopSitesSnappedView", "chrome://browser/content/TopSites.js"],
+ ["TopSitesStartView", "chrome://browser/content/TopSites.js"],
+ ["InputSourceHelper", "chrome://browser/content/input.js"],
+ ["PageActions", "chrome://browser/content/PageActions.js"],
+ ["Sanitizer", "chrome://browser/content/sanitize.js"],
+ ["SSLExceptions", "chrome://browser/content/exceptions.js"],
+#ifdef MOZ_SERVICES_SYNC
+ ["WeaveGlue", "chrome://browser/content/sync.js"],
+ ["SyncPairDevice", "chrome://browser/content/sync.js"],
+ ["RemoteTabsView", "chrome://browser/content/RemoteTabs.js"],
+ ["RemoteTabsPanelView", "chrome://browser/content/RemoteTabs.js"],
+ ["RemoteTabsStartView", "chrome://browser/content/RemoteTabs.js"],
+#endif
+].forEach(function (aScript) {
+ let [name, script] = aScript;
+ XPCOMUtils.defineLazyGetter(window, name, function() {
+ let sandbox = {};
+ Services.scriptloader.loadSubScript(script, sandbox);
+ return sandbox[name];
+ });
+});
+
+#ifdef MOZ_SERVICES_SYNC
+XPCOMUtils.defineLazyGetter(this, "Weave", function() {
+ Components.utils.import("resource://services-sync/main.js");
+ return Weave;
+});
+#endif
+
+/*
+ * Delay load some global scripts using a custom namespace
+ */
+XPCOMUtils.defineLazyGetter(this, "GlobalOverlay", function() {
+ let GlobalOverlay = {};
+ Services.scriptloader.loadSubScript("chrome://global/content/globalOverlay.js", GlobalOverlay);
+ return GlobalOverlay;
+});
+
+XPCOMUtils.defineLazyGetter(this, "ContentAreaUtils", function() {
+ let ContentAreaUtils = {};
+ Services.scriptloader.loadSubScript("chrome://global/content/contentAreaUtils.js", ContentAreaUtils);
+ return ContentAreaUtils;
+});
+
+XPCOMUtils.defineLazyGetter(this, "ZoomManager", function() {
+ let sandbox = {};
+ Services.scriptloader.loadSubScript("chrome://global/content/viewZoomOverlay.js", sandbox);
+ return sandbox.ZoomManager;
+});
diff --git a/browser/metro/base/content/browser-ui.js b/browser/metro/base/content/browser-ui.js
new file mode 100644
index 000000000000..ee47f131ee37
--- /dev/null
+++ b/browser/metro/base/content/browser-ui.js
@@ -0,0 +1,1638 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Constants
+ */
+
+// BrowserUI.update(state) constants. Currently passed in
+// but update doesn't pay attention to them. Can we remove?
+const TOOLBARSTATE_LOADING = 1;
+const TOOLBARSTATE_LOADED = 2;
+
+// delay for ContextUI's dismissWithDelay
+const kHideContextAndTrayDelayMsec = 3000;
+
+// delay when showing the tab bar briefly as a new tab opens
+const kNewTabAnimationDelayMsec = 500;
+
+
+// Page for which the start UI is shown
+const kStartOverlayURI = "about:start";
+
+/**
+ * Cache of commonly used elements.
+ */
+
+let Elements = {};
+[
+ ["contentShowing", "bcast_contentShowing"],
+ ["urlbarState", "bcast_urlbarState"],
+ ["windowState", "bcast_windowState"],
+ ["mainKeyset", "mainKeyset"],
+ ["stack", "stack"],
+ ["tabList", "tabs"],
+ ["tabs", "tabs-container"],
+ ["controls", "browser-controls"],
+ ["panelUI", "panel-container"],
+ ["startUI", "start-container"],
+ ["tray", "tray"],
+ ["toolbar", "toolbar"],
+ ["browsers", "browsers"],
+ ["appbar", "appbar"],
+ ["contentViewport", "content-viewport"],
+ ["progress", "progress-control"],
+ ["contentNavigator", "content-navigator"],
+ ["aboutFlyout", "about-flyoutpanel"],
+ ["prefsFlyout", "prefs-flyoutpanel"]
+].forEach(function (aElementGlobal) {
+ let [name, id] = aElementGlobal;
+ XPCOMUtils.defineLazyGetter(Elements, name, function() {
+ return document.getElementById(id);
+ });
+});
+
+/**
+ * Cache of commonly used string bundles.
+ */
+
+var Strings = {};
+[
+ ["browser", "chrome://browser/locale/browser.properties"],
+ ["brand", "chrome://branding/locale/brand.properties"]
+].forEach(function (aStringBundle) {
+ let [name, bundle] = aStringBundle;
+ XPCOMUtils.defineLazyGetter(Strings, name, function() {
+ return Services.strings.createBundle(bundle);
+ });
+});
+
+var BrowserUI = {
+ get _edit() { return document.getElementById("urlbar-edit"); },
+ get _back() { return document.getElementById("cmd_back"); },
+ get _forward() { return document.getElementById("cmd_forward"); },
+
+ init: function() {
+ // listen content messages
+ messageManager.addMessageListener("DOMTitleChanged", this);
+ messageManager.addMessageListener("DOMWillOpenModalDialog", this);
+ messageManager.addMessageListener("DOMWindowClose", this);
+
+ messageManager.addMessageListener("Browser:OpenURI", this);
+ messageManager.addMessageListener("Browser:SaveAs:Return", this);
+
+ // listening escape to dismiss dialog on VK_ESCAPE
+ window.addEventListener("keypress", this, true);
+
+ window.addEventListener("MozPrecisePointer", this, true);
+ window.addEventListener("MozImprecisePointer", this, true);
+
+ Services.prefs.addObserver("browser.tabs.tabsOnly", this, false);
+ Services.obs.addObserver(this, "metro_viewstate_changed", false);
+
+ // Init core UI modules
+ ContextUI.init();
+ StartUI.init();
+ PanelUI.init();
+ IdentityUI.init();
+ if (Browser.getHomePage() === "about:start") {
+ StartUI.show();
+ }
+ FlyoutPanelsUI.init();
+
+ // show the right toolbars, awesomescreen, etc for the os viewstate
+ BrowserUI._adjustDOMforViewState();
+
+ // We can delay some initialization until after startup. We wait until
+ // the first page is shown, then dispatch a UIReadyDelayed event.
+ messageManager.addMessageListener("pageshow", function() {
+ if (getBrowser().currentURI.spec == "about:blank")
+ return;
+
+ messageManager.removeMessageListener("pageshow", arguments.callee, true);
+
+ setTimeout(function() {
+ let event = document.createEvent("Events");
+ event.initEvent("UIReadyDelayed", true, false);
+ window.dispatchEvent(event);
+ }, 0);
+ });
+
+ // Only load IndexedDB.js when we actually need it. A general fix will happen in bug 647079.
+ messageManager.addMessageListener("IndexedDB:Prompt", function(aMessage) {
+ return IndexedDB.receiveMessage(aMessage);
+ });
+
+ // Delay the panel UI and Sync initialization
+ window.addEventListener("UIReadyDelayed", function(aEvent) {
+ Util.dumpLn("* delay load started...");
+ window.removeEventListener(aEvent.type, arguments.callee, false);
+
+ // Login Manager and Form History initialization
+ Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
+ Cc["@mozilla.org/satchel/form-history;1"].getService(Ci.nsIFormHistory2);
+
+ messageManager.addMessageListener("Browser:MozApplicationManifest", OfflineApps);
+
+ try {
+ BrowserUI._updateTabsOnly();
+ Downloads.init();
+ DialogUI.init();
+ FormHelperUI.init();
+ FindHelperUI.init();
+ FullScreenVideo.init();
+ PdfJs.init();
+#ifdef MOZ_SERVICES_SYNC
+ WeaveGlue.init();
+#endif
+ } catch(ex) {
+ Util.dumpLn("Exception in delay load module:", ex.message);
+ }
+
+ try {
+ SettingsCharm.init();
+ } catch (ex) {
+ }
+
+ try {
+ // XXX This is currently failing
+ CapturePickerUI.init();
+ } catch(ex) {
+ Util.dumpLn("Exception in CapturePickerUI:", ex.message);
+ }
+
+#ifdef MOZ_UPDATER
+ // Check for updates in progress
+ let updatePrompt = Cc["@mozilla.org/updates/update-prompt;1"].createInstance(Ci.nsIUpdatePrompt);
+ updatePrompt.checkForUpdates();
+#endif
+
+ // check for left over crash reports and submit them if found.
+ if (BrowserUI.startupCrashCheck()) {
+ Browser.selectedTab = BrowserUI.newOrSelectTab("about:crash");
+ }
+ Util.dumpLn("* delay load complete.");
+ }, false);
+
+#ifndef MOZ_OFFICIAL_BRANDING
+ setTimeout(function() {
+ let startup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup).getStartupInfo();
+ for (let name in startup) {
+ if (name != "process")
+ Services.console.logStringMessage("[timing] " + name + ": " + (startup[name] - startup.process) + "ms");
+ }
+ }, 3000);
+#endif
+ },
+
+ uninit: function() {
+ messageManager.removeMessageListener("Browser:MozApplicationManifest", OfflineApps);
+
+ PanelUI.uninit();
+ StartUI.uninit();
+ Downloads.uninit();
+ SettingsCharm.uninit();
+ },
+
+
+ /*********************************
+ * Content visibility
+ */
+
+ get isContentShowing() {
+ return Elements.contentShowing.getAttribute("disabled") != true;
+ },
+
+ showContent: function showContent(aURI) {
+ DialogUI.closeAllDialogs();
+ StartUI.update(aURI);
+ ContextUI.dismissTabs();
+ ContextUI.dismissAppbar();
+ FlyoutPanelsUI.hide();
+ PanelUI.hide();
+ },
+
+ /*********************************
+ * Crash reporting
+ */
+
+ get CrashSubmit() {
+ delete this.CrashSubmit;
+ Cu.import("resource://gre/modules/CrashSubmit.jsm", this);
+ return this.CrashSubmit;
+ },
+
+ startupCrashCheck: function startupCrashCheck() {
+#ifdef MOZ_CRASHREPORTER
+ if (!Services.prefs.getBoolPref("app.reportCrashes"))
+ return false;
+ if (CrashReporter.enabled) {
+ var lastCrashID = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo).lastRunCrashID;
+ if (lastCrashID.length) {
+ this.CrashSubmit.submit(lastCrashID);
+ return true;
+ }
+ }
+#endif
+ return false;
+ },
+
+
+ /*********************************
+ * Navigation
+ */
+
+ update: function(aState) {
+ this._updateToolbar();
+ },
+
+ getDisplayURI: function(browser) {
+ let uri = browser.currentURI;
+ try {
+ uri = gURIFixup.createExposableURI(uri);
+ } catch (ex) {}
+
+ return uri.spec;
+ },
+
+ /* Set the location to the current content */
+ updateURI: function(aOptions) {
+ aOptions = aOptions || {};
+
+ let uri = this.getDisplayURI(Browser.selectedBrowser);
+ let cleanURI = Util.isURLEmpty(uri) ? "" : uri;
+ this._setURI(cleanURI);
+
+ if ("captionOnly" in aOptions && aOptions.captionOnly)
+ return;
+
+ StartUI.update(uri);
+ this._updateButtons();
+ this._updateToolbar();
+ },
+
+ goToURI: function(aURI) {
+ aURI = aURI || this._edit.value;
+ if (!aURI)
+ return;
+
+ // Make sure we're online before attempting to load
+ Util.forceOnline();
+
+ BrowserUI.showContent(aURI);
+ content.focus();
+ this._setURI(aURI);
+
+ let postData = {};
+ aURI = Browser.getShortcutOrURI(aURI, postData);
+ Browser.loadURI(aURI, { flags: Ci.nsIWebNavigation.LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP, postData: postData });
+
+ // Delay doing the fixup so the raw URI is passed to loadURIWithFlags
+ // and the proper third-party fixup can be done
+ let fixupFlags = Ci.nsIURIFixup.FIXUP_FLAG_ALLOW_KEYWORD_LOOKUP;
+ let uri = gURIFixup.createFixupURI(aURI, fixupFlags);
+ gHistSvc.markPageAsTyped(uri);
+
+ this._titleChanged(Browser.selectedBrowser);
+ },
+
+ handleUrlbarEnter: function handleUrlbarEnter(aEvent) {
+ let url = this._edit.value;
+ if (aEvent instanceof KeyEvent)
+ url = this._canonizeURL(url, aEvent);
+ this.goToURI(url);
+ },
+
+ _canonizeURL: function _canonizeURL(aUrl, aTriggeringEvent) {
+ if (!aUrl)
+ return "";
+
+ // Only add the suffix when the URL bar value isn't already "URL-like",
+ // and only if we get a keyboard event, to match user expectations.
+ if (/^\s*[^.:\/\s]+(?:\/.*|\s*)$/i.test(aUrl)) {
+ let accel = aTriggeringEvent.ctrlKey;
+ let shift = aTriggeringEvent.shiftKey;
+ let suffix = "";
+
+ switch (true) {
+ case (accel && shift):
+ suffix = ".org/";
+ break;
+ case (shift):
+ suffix = ".net/";
+ break;
+ case (accel):
+ try {
+ suffix = gPrefService.getCharPref("browser.fixup.alternate.suffix");
+ if (suffix.charAt(suffix.length - 1) != "/")
+ suffix += "/";
+ } catch(e) {
+ suffix = ".com/";
+ }
+ break;
+ }
+
+ if (suffix) {
+ // trim leading/trailing spaces (bug 233205)
+ aUrl = aUrl.trim();
+
+ // Tack www. and suffix on. If user has appended directories, insert
+ // suffix before them (bug 279035). Be careful not to get two slashes.
+ let firstSlash = aUrl.indexOf("/");
+ if (firstSlash >= 0) {
+ aUrl = aUrl.substring(0, firstSlash) + suffix + aUrl.substring(firstSlash + 1);
+ } else {
+ aUrl = aUrl + suffix;
+ }
+ aUrl = "http://www." + aUrl;
+ }
+ }
+ return aUrl;
+ },
+
+ doOpenSearch: function doOpenSearch(aName) {
+ // save the current value of the urlbar
+ let searchValue = this._edit.value;
+
+ // Make sure we're online before attempting to load
+ Util.forceOnline();
+ BrowserUI.showContent();
+
+ let engine = Services.search.getEngineByName(aName);
+ let submission = engine.getSubmission(searchValue, null);
+ Browser.loadURI(submission.uri.spec, { postData: submission.postData });
+
+ // loadURI may open a new tab, so get the selectedBrowser afterward.
+ Browser.selectedBrowser.userTypedValue = submission.uri.spec;
+ this._titleChanged(Browser.selectedBrowser);
+ },
+
+ /*********************************
+ * Tab management
+ */
+
+ newTab: function newTab(aURI, aOwner) {
+ aURI = aURI || kStartOverlayURI;
+ let tab = Browser.addTab(aURI, true, aOwner);
+ ContextUI.peekTabs();
+ return tab;
+ },
+
+ newOrSelectTab: function newOrSelectTab(aURI, aOwner) {
+ let tabs = Browser.tabs;
+ for (let i = 0; i < tabs.length; i++) {
+ if (tabs[i].browser.currentURI.spec == aURI) {
+ Browser.selectedTab = tabs[i];
+ return;
+ }
+ }
+ this.newTab(aURI, aOwner);
+ },
+
+ closeTab: function closeTab(aTab) {
+ // If we only have one tab, open a new one
+ if (Browser.tabs.length == 1)
+ Browser.addTab(Browser.getHomePage());
+
+ // If no tab is passed in, assume the current tab
+ Browser.closeTab(aTab || Browser.selectedTab);
+ },
+
+ /**
+ * Re-open a closed tab.
+ * @param aIndex
+ * The index of the tab (via nsSessionStore.getClosedTabData)
+ * @returns a reference to the reopened tab.
+ */
+ undoCloseTab: function undoCloseTab(aIndex) {
+ var tab = null;
+ aIndex = aIndex || 0;
+ var ss = Cc["@mozilla.org/browser/sessionstore;1"].
+ getService(Ci.nsISessionStore);
+ if (ss.getClosedTabCount(window) > (aIndex)) {
+ tab = ss.undoCloseTab(window, aIndex);
+ }
+ return tab;
+ },
+
+ // Useful for when we've received an event to close a particular DOM window.
+ // Since we don't have windows, we want to close the corresponding tab.
+ closeTabForBrowser: function closeTabForBrowser(aBrowser) {
+ // Find the relevant tab, and close it.
+ let browsers = Browser.browsers;
+ for (let i = 0; i < browsers.length; i++) {
+ if (browsers[i] == aBrowser) {
+ Browser.closeTab(Browser.getTabAtIndex(i));
+ return { preventDefault: true };
+ }
+ }
+
+ return {};
+ },
+
+ selectTab: function selectTab(aTab) {
+ Browser.selectedTab = aTab;
+ },
+
+ selectTabAndDismiss: function selectTabAndDismiss(aTab) {
+ this.selectTab(aTab);
+ ContextUI.dismiss();
+ },
+
+ selectTabAtIndex: function selectTabAtIndex(aIndex) {
+ // count backwards for aIndex < 0
+ if (aIndex < 0)
+ aIndex += Browser._tabs.length;
+
+ if (aIndex >= 0 && aIndex < Browser._tabs.length)
+ Browser.selectedTab = Browser._tabs[aIndex];
+ },
+
+ selectNextTab: function selectNextTab() {
+ if (Browser._tabs.length == 1 || !Browser.selectedTab) {
+ return;
+ }
+
+ let tabIndex = Browser._tabs.indexOf(Browser.selectedTab) + 1;
+ if (tabIndex >= Browser._tabs.length) {
+ tabIndex = 0;
+ }
+
+ Browser.selectedTab = Browser._tabs[tabIndex];
+ },
+
+ selectPreviousTab: function selectPreviousTab() {
+ if (Browser._tabs.length == 1 || !Browser.selectedTab) {
+ return;
+ }
+
+ let tabIndex = Browser._tabs.indexOf(Browser.selectedTab) - 1;
+ if (tabIndex < 0) {
+ tabIndex = Browser._tabs.length - 1;
+ }
+
+ Browser.selectedTab = Browser._tabs[tabIndex];
+ },
+
+ // Used for when we're about to open a modal dialog,
+ // and want to ensure the opening tab is in front.
+ selectTabForBrowser: function selectTabForBrowser(aBrowser) {
+ for (let i = 0; i < Browser.tabs.length; i++) {
+ if (Browser._tabs[i].browser == aBrowser) {
+ Browser.selectedTab = Browser.tabs[i];
+ break;
+ }
+ }
+ },
+
+ updateUIFocus: function _updateUIFocus() {
+ if (Elements.contentShowing.getAttribute("disabled") == "true" && Browser.selectedBrowser)
+ Browser.selectedBrowser.messageManager.sendAsyncMessage("Browser:Blur", { });
+ },
+
+ blurFocusedElement: function blurFocusedElement() {
+ let focusedElement = document.commandDispatcher.focusedElement;
+ if (focusedElement)
+ focusedElement.blur();
+ },
+
+ // If the user types in the address bar, cancel pending
+ // navbar autohide if set.
+ navEditKeyPress: function navEditKeyPress() {
+ ContextUI.cancelDismiss();
+ },
+
+
+ /*********************************
+ * Conventional tabs
+ */
+
+ // Tabsonly displays the url bar with conventional tabs. Also
+ // the tray does not auto hide.
+ get isTabsOnly() {
+ return Services.prefs.getBoolPref("browser.tabs.tabsOnly");
+ },
+
+ _updateTabsOnly: function _updateTabsOnly() {
+ if (this.isTabsOnly) {
+ Elements.windowState.setAttribute("tabsonly", "true");
+ } else {
+ Elements.windowState.removeAttribute("tabsonly");
+ }
+ },
+
+ observe: function BrowserUI_observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "nsPref:changed":
+ if (aData == "browser.tabs.tabsOnly")
+ this._updateTabsOnly();
+ break;
+ case "metro_viewstate_changed":
+ this._adjustDOMforViewState();
+ break;
+ }
+ },
+
+ /*********************************
+ * Internal utils
+ */
+
+ _adjustDOMforViewState: function() {
+ if (MetroUtils.immersive) {
+ let currViewState = "";
+ switch (MetroUtils.snappedState) {
+ case Ci.nsIWinMetroUtils.fullScreenLandscape:
+ currViewState = "landscape";
+ break;
+ case Ci.nsIWinMetroUtils.fullScreenPortrait:
+ currViewState = "portrait";
+ break;
+ case Ci.nsIWinMetroUtils.filled:
+ currViewState = "filled";
+ break;
+ case Ci.nsIWinMetroUtils.snapped:
+ currViewState = "snapped";
+ break;
+ }
+ Elements.windowState.setAttribute("viewstate", currViewState);
+ }
+ // content navigator helper
+ document.getElementById("content-navigator").contentHasChanged();
+ },
+
+ _titleChanged: function(aBrowser) {
+ let url = this.getDisplayURI(aBrowser);
+
+ let tabCaption;
+ if (aBrowser.contentTitle) {
+ tabCaption = aBrowser.contentTitle;
+ } else if (!Util.isURLEmpty(aBrowser.userTypedValue)) {
+ tabCaption = aBrowser.userTypedValue;
+ } else if (!Util.isURLEmpty(url)) {
+ tabCaption = url;
+ } else {
+ tabCaption = Strings.browser.GetStringFromName("tabs.emptyTabTitle");
+ }
+
+ let tab = Browser.getTabForBrowser(aBrowser);
+ if (tab)
+ tab.chromeTab.updateTitle(tabCaption);
+ },
+
+ _updateButtons: function _updateButtons() {
+ let browser = Browser.selectedBrowser;
+ this._back.setAttribute("disabled", !browser.canGoBack);
+ this._forward.setAttribute("disabled", !browser.canGoForward);
+ },
+
+ _updateToolbar: function _updateToolbar() {
+ let mode = Elements.urlbarState.getAttribute("mode");
+ let isLoading = Browser.selectedTab.isLoading();
+
+ if (isLoading && mode != "loading")
+ Elements.urlbarState.setAttribute("mode", "loading");
+ else if (!isLoading && mode != "edit")
+ Elements.urlbarState.setAttribute("mode", "view");
+ },
+
+ _setURI: function _setURI(aURL) {
+ this._edit.value = aURL;
+ },
+
+ _urlbarClicked: function _urlbarClicked() {
+ // If the urlbar is not already focused, focus it and select the contents.
+ if (Elements.urlbarState.getAttribute("mode") != "edit")
+ this._editURI();
+ },
+
+ _editURI: function _editURI() {
+ this._edit.focus();
+ this._edit.select();
+
+ Elements.urlbarState.setAttribute("mode", "edit");
+ StartUI.show();
+ ContextUI.dismissTabs();
+ },
+
+ _urlbarBlurred: function _urlbarBlurred() {
+ let state = Elements.urlbarState;
+ if (state.getAttribute("mode") == "edit")
+ state.removeAttribute("mode");
+ this._updateToolbar();
+ },
+
+ _closeOrQuit: function _closeOrQuit() {
+ // Close active dialog, if we have one. If not then close the application.
+ if (!BrowserUI.isContentShowing()) {
+ BrowserUI.showContent();
+ } else {
+ // Check to see if we should really close the window
+ if (Browser.closing()) {
+ window.close();
+ let appStartup = Cc["@mozilla.org/toolkit/app-startup;1"].getService(Ci.nsIAppStartup);
+ appStartup.quit(Ci.nsIAppStartup.eForceQuit);
+ }
+ }
+ },
+
+ _onPreciseInput: function _onPreciseInput() {
+ document.getElementById("bcast_preciseInput").setAttribute("input", "precise");
+ let uri = Util.makeURI("chrome://browser/content/cursor.css");
+ if (StyleSheetSvc.sheetRegistered(uri, Ci.nsIStyleSheetService.AGENT_SHEET)) {
+ StyleSheetSvc.unregisterSheet(uri,
+ Ci.nsIStyleSheetService.AGENT_SHEET);
+ }
+ },
+
+ _onImpreciseInput: function _onImpreciseInput() {
+ document.getElementById("bcast_preciseInput").setAttribute("input", "imprecise");
+ let uri = Util.makeURI("chrome://browser/content/cursor.css");
+ if (!StyleSheetSvc.sheetRegistered(uri, Ci.nsIStyleSheetService.AGENT_SHEET)) {
+ StyleSheetSvc.loadAndRegisterSheet(uri,
+ Ci.nsIStyleSheetService.AGENT_SHEET);
+ }
+ },
+
+ /*********************************
+ * Event handling
+ */
+
+ handleEvent: function handleEvent(aEvent) {
+ var target = aEvent.target;
+ switch (aEvent.type) {
+ // Window events
+ case "keypress":
+ if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE)
+ this.handleEscape(aEvent);
+ break;
+ case "MozPrecisePointer":
+ this._onPreciseInput();
+ break;
+ case "MozImprecisePointer":
+ this._onImpreciseInput();
+ break;
+ }
+ },
+
+ // Checks if various different parts of the UI is visible and closes
+ // them one at a time.
+ handleEscape: function (aEvent) {
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+
+ if (this._edit.popupOpen) {
+ this._edit.closePopup();
+ StartUI.hide();
+ ContextUI.dismiss();
+ return;
+ }
+
+ // Check open popups
+ if (DialogUI._popup) {
+ DialogUI._hidePopup();
+ return;
+ }
+
+ // Check open dialogs
+ let dialog = DialogUI.activeDialog;
+ if (dialog) {
+ dialog.close();
+ return;
+ }
+
+ // Check open modal elements
+ if (DialogUI.modals.length > 0)
+ return;
+
+ // Check open panel
+ if (PanelUI.isVisible) {
+ PanelUI.hide();
+ return;
+ }
+
+ // Check content helper
+ let contentHelper = document.getElementById("content-navigator");
+ if (contentHelper.isActive) {
+ contentHelper.model.hide();
+ return;
+ }
+
+ if (StartUI.hide()) {
+ // When escaping from the start screen, hide the toolbar too.
+ ContextUI.dismiss();
+ return;
+ }
+
+ if (ContextUI.dismiss()) {
+ return;
+ }
+
+ if (Browser.selectedTab.isLoading()) {
+ Browser.selectedBrowser.stop();
+ return;
+ }
+ },
+
+ handleBackspace: function handleBackspace() {
+ switch (Services.prefs.getIntPref("browser.backspace_action")) {
+ case 0:
+ CommandUpdater.doCommand("cmd_back");
+ break;
+ case 1:
+ CommandUpdater.doCommand("cmd_scrollPageUp");
+ break;
+ }
+ },
+
+ handleShiftBackspace: function handleShiftBackspace() {
+ switch (Services.prefs.getIntPref("browser.backspace_action")) {
+ case 0:
+ CommandUpdater.doCommand("cmd_forward");
+ break;
+ case 1:
+ CommandUpdater.doCommand("cmd_scrollPageDown");
+ break;
+ }
+ },
+
+ receiveMessage: function receiveMessage(aMessage) {
+ let browser = aMessage.target;
+ let json = aMessage.json;
+ switch (aMessage.name) {
+ case "DOMTitleChanged":
+ this._titleChanged(browser);
+ break;
+ case "DOMWillOpenModalDialog":
+ this.selectTabForBrowser(browser);
+ break;
+ case "DOMWindowClose":
+ return this.closeTabForBrowser(browser);
+ break;
+ // XXX this and content's sender are a little warped
+ case "Browser:OpenURI":
+ let referrerURI = null;
+ if (json.referrer)
+ referrerURI = Services.io.newURI(json.referrer, null, null);
+ //Browser.addTab(json.uri, json.bringFront, Browser.selectedTab, { referrerURI: referrerURI });
+ this.goToURI(json.uri);
+ break;
+ }
+
+ return {};
+ },
+
+ supportsCommand : function(cmd) {
+ var isSupported = false;
+ switch (cmd) {
+ case "cmd_back":
+ case "cmd_forward":
+ case "cmd_reload":
+ case "cmd_forceReload":
+ case "cmd_stop":
+ case "cmd_go":
+ case "cmd_home":
+ case "cmd_openLocation":
+ case "cmd_addBookmark":
+ case "cmd_bookmarks":
+ case "cmd_history":
+ case "cmd_remoteTabs":
+ case "cmd_quit":
+ case "cmd_close":
+ case "cmd_newTab":
+ case "cmd_closeTab":
+ case "cmd_undoCloseTab":
+ case "cmd_actions":
+ case "cmd_panel":
+ case "cmd_flyout_back":
+ case "cmd_sanitize":
+ case "cmd_zoomin":
+ case "cmd_zoomout":
+ case "cmd_volumeLeft":
+ case "cmd_volumeRight":
+ isSupported = true;
+ break;
+ default:
+ isSupported = false;
+ break;
+ }
+ return isSupported;
+ },
+
+ isCommandEnabled : function(cmd) {
+ let elem = document.getElementById(cmd);
+ if (elem && elem.getAttribute("disabled") == "true")
+ return false;
+ return true;
+ },
+
+ doCommand : function(cmd) {
+ if (!this.isCommandEnabled(cmd))
+ return;
+ let browser = getBrowser();
+ switch (cmd) {
+ case "cmd_back":
+ browser.goBack();
+ break;
+ case "cmd_forward":
+ browser.goForward();
+ break;
+ case "cmd_reload":
+ browser.reload();
+ break;
+ case "cmd_forceReload":
+ {
+ // Simulate a new page
+ browser.lastLocation = null;
+
+ const reloadFlags = Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_PROXY |
+ Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CACHE;
+ browser.reloadWithFlags(reloadFlags);
+ break;
+ }
+ case "cmd_stop":
+ browser.stop();
+ break;
+ case "cmd_go":
+ this.goToURI();
+ break;
+ case "cmd_home":
+ this.goToURI(Browser.getHomePage());
+ break;
+ case "cmd_openLocation":
+ ContextUI.displayNavbar();
+ this._editURI();
+ break;
+ case "cmd_addBookmark":
+ Elements.appbar.show();
+ Appbar.onStarButton(true);
+ break;
+ case "cmd_bookmarks":
+ PanelUI.show("bookmarks-container");
+ break;
+ case "cmd_history":
+ PanelUI.show("history-container");
+ break;
+ case "cmd_remoteTabs":
+ if (Weave.Status.checkSetup() == Weave.CLIENT_NOT_CONFIGURED) {
+ WeaveGlue.open();
+ } else {
+ PanelUI.show("remotetabs-container");
+ }
+ break;
+ case "cmd_quit":
+ // Only close one window
+ this._closeOrQuit();
+ break;
+ case "cmd_close":
+ this._closeOrQuit();
+ break;
+ case "cmd_newTab":
+ this.newTab();
+ this._editURI();
+ break;
+ case "cmd_closeTab":
+ this.closeTab();
+ break;
+ case "cmd_undoCloseTab":
+ this.undoCloseTab();
+ break;
+ case "cmd_sanitize":
+ {
+ let title = Strings.browser.GetStringFromName("clearPrivateData.title");
+ let message = Strings.browser.GetStringFromName("clearPrivateData.message");
+ let clear = Services.prompt.confirm(window, title, message);
+ if (clear) {
+ // disable the button temporarily to indicate something happened
+ let button = document.getElementById("prefs-clear-data");
+ button.disabled = true;
+ setTimeout(function() { button.disabled = false; }, 5000);
+
+ Sanitizer.sanitize();
+ }
+ break;
+ }
+ case "cmd_flyout_back":
+ FlyoutPanelsUI.hide();
+ MetroUtils.showSettingsFlyout();
+ break;
+ case "cmd_panel":
+ PanelUI.toggle();
+ break;
+ case "cmd_zoomin":
+ Browser.zoom(-1);
+ break;
+ case "cmd_zoomout":
+ Browser.zoom(1);
+ break;
+ case "cmd_volumeLeft":
+ // Zoom in (portrait) or out (landscape)
+ Browser.zoom(Util.isPortrait() ? -1 : 1);
+ break;
+ case "cmd_volumeRight":
+ // Zoom out (portrait) or in (landscape)
+ Browser.zoom(Util.isPortrait() ? 1 : -1);
+ break;
+ }
+ }
+};
+
+/**
+ * Tracks whether context UI (app bar, tab bar, url bar) is shown or hidden.
+ * Manages events to summon and hide the context UI.
+ */
+var ContextUI = {
+ _expandable: true,
+ _hidingId: 0,
+
+ /*******************************************
+ * init
+ */
+
+ init: function init() {
+ Elements.browsers.addEventListener("mousedown", this, true);
+ Elements.browsers.addEventListener("touchstart", this, true);
+ window.addEventListener("MozEdgeUIGesture", this, true);
+ window.addEventListener("keypress", this, true);
+ window.addEventListener("KeyboardChanged", this, false);
+
+ Elements.tray.addEventListener("transitionend", this, true);
+
+ Appbar.init();
+ },
+
+ /*******************************************
+ * Context UI state getters & setters
+ */
+
+ get isVisible() { return Elements.tray.hasAttribute("visible"); },
+ get isExpanded() { return Elements.tray.hasAttribute("expanded"); },
+ get isExpandable() { return this._expandable; },
+
+ set isExpandable(aFlag) {
+ this._expandable = aFlag;
+ if (!this._expandable)
+ this.dismiss();
+ },
+
+ /*******************************************
+ * Context UI state control
+ */
+
+ toggle: function toggle() {
+ if (!this._expandable) {
+ // exandable setter takes care of resetting state
+ // so if we're not expandable, there's nothing to do here
+ return;
+ }
+ // if we're not showing, show
+ if (!this.dismiss()) {
+ dump("* ContextUI is hidden, show it\n");
+ this.show();
+ }
+ },
+
+ // show all context UI
+ // returns true if any non-visible UI was shown
+ show: function() {
+ let shown = false;
+ if (!this.isExpanded) {
+ // show the tab tray
+ this._setIsExpanded(true);
+ shown = true;
+ }
+ if (!this.isVisible) {
+ // show the navbar
+ this._setIsVisible(true);
+ shown = true;
+ }
+ if (!Elements.appbar.isShowing) {
+ // show the appbar
+ Elements.appbar.show();
+ shown = true;
+ }
+
+ this._clearDelayedTimeout();
+ if (shown) {
+ ContentAreaObserver.update(window.innerWidth, window.innerHeight);
+ }
+ return shown;
+ },
+
+ // Display the nav bar
+ displayNavbar: function displayNavbar() {
+ this._clearDelayedTimeout();
+ this._setIsVisible(true, true);
+ },
+
+ // Display the toolbar and tabs
+ displayTabs: function displayTabs() {
+ this._clearDelayedTimeout();
+ this._setIsVisible(true, true);
+ this._setIsExpanded(true, true);
+ },
+
+ /** Briefly show the tab bar and then hide it */
+ peekTabs: function peekTabs() {
+ if (this.isExpanded)
+ return;
+
+ Elements.tabs.addEventListener("animationend", function onAnimationEnd() {
+ Elements.tabs.removeEventListener("animationend", onAnimationEnd);
+ ContextUI.dismissWithDelay(kNewTabAnimationDelayMsec);
+ });
+ this.displayTabs();
+ },
+
+ // Dismiss all context UI.
+ // Returns true if any visible UI was dismissed.
+ dismiss: function dismiss() {
+ let dismissed = false;
+ if (this.isExpanded) {
+ this._setIsExpanded(false);
+ dismissed = true;
+ }
+ if (this.isVisible && !StartUI.isStartURI()) {
+ this._setIsVisible(false);
+ dismissed = true;
+ }
+ if (Elements.appbar.isShowing) {
+ this.dismissAppbar();
+ dismissed = true;
+ }
+ this._clearDelayedTimeout();
+ if (dismissed) {
+ ContentAreaObserver.update(window.innerWidth, window.innerHeight);
+ }
+ return dismissed;
+ },
+
+ // Dismiss all context ui after a delay
+ dismissWithDelay: function dismissWithDelay(aDelay) {
+ aDelay = aDelay || kHideContextAndTrayDelayMsec;
+ this._clearDelayedTimeout();
+ this._hidingId = setTimeout(function () {
+ ContextUI.dismiss();
+ }, aDelay);
+ },
+
+ // Cancel any pending delayed dismiss
+ cancelDismiss: function cancelDismiss() {
+ this._clearDelayedTimeout();
+ },
+
+ dismissTabs: function dimissTabs() {
+ this._clearDelayedTimeout();
+ this._setIsExpanded(false, true);
+ },
+
+ dismissAppbar: function dismissAppbar() {
+ this._fire("MozAppbarDismiss");
+ },
+
+ /*******************************************
+ * Internal tray state setters
+ */
+
+ // url bar state
+ _setIsVisible: function _setIsVisible(aFlag, setSilently) {
+ if (this.isVisible == aFlag)
+ return;
+
+ if (aFlag)
+ Elements.tray.setAttribute("visible", "true");
+ else
+ Elements.tray.removeAttribute("visible");
+
+ if (!aFlag) {
+ content.focus();
+ }
+
+ if (!setSilently)
+ this._fire(aFlag ? "MozContextUIShow" : "MozContextUIDismiss");
+ },
+
+ // tab tray state
+ _setIsExpanded: function _setIsExpanded(aFlag, setSilently) {
+ // if the tray can't be expanded because we're in
+ // tabsonly mode, don't expand it.
+ if (!this.isExpandable || this.isExpanded == aFlag)
+ return;
+
+ if (aFlag)
+ Elements.tray.setAttribute("expanded", "true");
+ else
+ Elements.tray.removeAttribute("expanded");
+
+ if (!setSilently)
+ this._fire("MozContextUIExpand");
+ },
+
+ /*******************************************
+ * Internal utils
+ */
+
+ _clearDelayedTimeout: function _clearDelayedTimeout() {
+ if (this._hidingId) {
+ clearTimeout(this._hidingId);
+ this._hidingId = 0;
+ }
+ },
+
+ /*******************************************
+ * Events
+ */
+
+ _onEdgeUIEvent: function _onEdgeUIEvent(aEvent) {
+ this._clearDelayedTimeout();
+ if (StartUI.hide()) {
+ this.dismiss();
+ return;
+ }
+ this.toggle();
+ },
+
+ handleEvent: function handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "MozEdgeUIGesture":
+ this._onEdgeUIEvent(aEvent);
+ break;
+ case "mousedown":
+ if (aEvent.button == 0 && this.isVisible)
+ this.dismiss();
+ break;
+ case "touchstart":
+ this.dismiss();
+ break;
+ case "keypress":
+ if (String.fromCharCode(aEvent.which) == "z" &&
+ aEvent.getModifierState("Win"))
+ this.toggle();
+ break;
+ case "transitionend":
+ setTimeout(function () {
+ ContentAreaObserver.updateContentArea();
+ }, 0);
+ break;
+ case "KeyboardChanged":
+ this.dismissTabs();
+ break;
+ }
+ },
+
+ _fire: function (name) {
+ let event = document.createEvent("Events");
+ event.initEvent(name, true, true);
+ window.dispatchEvent(event);
+ }
+};
+
+var StartUI = {
+ get isVisible() { return this.isStartPageVisible || this.isFiltering; },
+ get isStartPageVisible() { return Elements.windowState.hasAttribute("startpage"); },
+ get isFiltering() { return Elements.windowState.hasAttribute("filtering"); },
+
+ get maxResultsPerSection() {
+ return Services.prefs.getIntPref("browser.display.startUI.maxresults");
+ },
+
+ sections: [
+ "TopSitesStartView",
+ "BookmarksStartView",
+ "HistoryStartView",
+ "RemoteTabsStartView"
+ ],
+
+ init: function init() {
+ Elements.startUI.addEventListener("autocompletestart", this, false);
+ Elements.startUI.addEventListener("autocompleteend", this, false);
+ Elements.startUI.addEventListener("contextmenu", this, false);
+
+ this.sections.forEach(function (sectionName) {
+ let section = window[sectionName];
+ if (section.init)
+ section.init();
+ });
+ },
+
+ uninit: function() {
+ this.sections.forEach(function (sectionName) {
+ let section = window[sectionName];
+ if (section.uninit)
+ section.uninit();
+ });
+ },
+
+ /** Show the Firefox start page / "new tab" page */
+ show: function show() {
+ if (this.isStartPageVisible)
+ return false;
+
+ ContextUI.displayNavbar();
+
+ Elements.contentShowing.setAttribute("disabled", "true");
+ Elements.windowState.setAttribute("startpage", "true");
+
+ this.sections.forEach(function (sectionName) {
+ let section = window[sectionName];
+ if (section.show)
+ section.show();
+ });
+ return true;
+ },
+
+ /** Show the autocomplete popup */
+ filter: function filter() {
+ if (this.isFiltering)
+ return;
+
+ BrowserUI._edit.openPopup();
+ Elements.windowState.setAttribute("filtering", "true");
+ },
+
+ /** Hide the autocomplete popup */
+ unfilter: function unfilter() {
+ if (!this.isFiltering)
+ return;
+
+ BrowserUI._edit.closePopup();
+ Elements.windowState.removeAttribute("filtering");
+ },
+
+ /** Hide the Firefox start page */
+ hide: function hide(aURI) {
+ aURI = aURI || Browser.selectedBrowser.currentURI.spec;
+ if (!this.isStartPageVisible || this.isStartURI(aURI))
+ return false;
+
+ Elements.contentShowing.removeAttribute("disabled");
+ Elements.windowState.removeAttribute("startpage");
+
+ this.unfilter();
+ return true;
+ },
+
+ /** Is the current tab supposed to show the Firefox start page? */
+ isStartURI: function isStartURI(aURI) {
+ aURI = aURI || Browser.selectedBrowser.currentURI.spec;
+ return aURI == kStartOverlayURI || aURI == "about:home";
+ },
+
+ /** Call this to show or hide the start page when switching tabs or pages */
+ update: function update(aURI) {
+ aURI = aURI || Browser.selectedBrowser.currentURI.spec;
+ if (this.isStartURI(aURI)) {
+ this.show();
+ } else if (aURI != "about:blank") { // about:blank is loaded briefly for new tabs; ignore it
+ this.hide(aURI);
+ }
+ },
+
+ handleEvent: function handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "autocompletestart":
+ this.filter();
+ break;
+ case "autocompleteend":
+ this.unfilter();
+ break;
+ case "contextmenu":
+ let event = document.createEvent("Events");
+ event.initEvent("MozEdgeUIGesture", true, false);
+ window.dispatchEvent(event);
+ break;
+ }
+ }
+};
+
+var FlyoutPanelsUI = {
+ get _aboutVersionLabel() {
+ return document.getElementById('about-version-label');
+ },
+
+ _initAboutPanel: function() {
+ // Include the build ID if this is an "a#" (nightly or aurora) build
+ let version = Services.appinfo.version;
+ if (/a\d+$/.test(version)) {
+ let buildID = Services.appinfo.appBuildID;
+ let buildDate = buildID.slice(0,4) + "-" + buildID.slice(4,6) +
+ "-" + buildID.slice(6,8);
+ this._aboutVersionLabel.textContent +=" (" + buildDate + ")";
+ }
+ },
+
+ init: function() {
+ this._initAboutPanel();
+ PreferencesPanelView.init();
+ },
+
+ hide: function() {
+ Elements.aboutFlyout.hide();
+ Elements.prefsFlyout.hide();
+ }
+};
+
+var PanelUI = {
+ get _panels() { return document.getElementById("panel-items"); },
+ get _switcher() { return document.getElementById("panel-view-switcher"); },
+
+ get isVisible() {
+ return !Elements.panelUI.hidden;
+ },
+
+ views: {
+ "bookmarks-container": "BookmarksPanelView",
+ "downloads-container": "DownloadsPanelView",
+ "console-container": "ConsolePanelView",
+ "remotetabs-container": "RemoteTabsPanelView",
+ "history-container" : "HistoryPanelView"
+ },
+
+ init: function() {
+ // Perform core init soon
+ setTimeout(function () {
+ for each (let viewName in this.views) {
+ let view = window[viewName];
+ if (view.init)
+ view.init();
+ }
+ }.bind(this), 0);
+
+ // Lazily run other initialization tasks when the views are shown
+ this._panels.addEventListener("ToolPanelShown", function(aEvent) {
+ let viewName = this.views[this._panels.selectedPanel.id];
+ let view = window[viewName];
+ if (view.show)
+ view.show();
+ }.bind(this), true);
+ },
+
+ uninit: function() {
+ for each (let viewName in this.views) {
+ let view = window[viewName];
+ if (view.uninit)
+ view.uninit();
+ }
+ },
+
+ switchPane: function switchPane(aPanelId) {
+ BrowserUI.blurFocusedElement();
+
+ let panel = aPanelId ? document.getElementById(aPanelId) : this._panels.selectedPanel;
+ let oldPanel = this._panels.selectedPanel;
+
+ if (oldPanel != panel) {
+ this._panels.selectedPanel = panel;
+ this._switcher.value = panel.id;
+
+ this._fire("ToolPanelHidden", oldPanel);
+ }
+
+ this._fire("ToolPanelShown", panel);
+ },
+
+ isPaneVisible: function isPaneVisible(aPanelId) {
+ return this.isVisible && this._panels.selectedPanel.id == aPanelId;
+ },
+
+ show: function show(aPanelId) {
+ Elements.panelUI.hidden = false;
+ Elements.contentShowing.setAttribute("disabled", "true");
+
+ this.switchPane(aPanelId);
+ },
+
+ hide: function hide() {
+ if (!this.isVisible)
+ return;
+
+ Elements.panelUI.hidden = true;
+ Elements.contentShowing.removeAttribute("disabled");
+ BrowserUI.blurFocusedElement();
+
+ this._fire("ToolPanelHidden", this._panels);
+ },
+
+ toggle: function toggle() {
+ if (this.isVisible) {
+ this.hide();
+ } else {
+ this.show();
+ }
+ },
+
+ _fire: function _fire(aName, anElement) {
+ let event = document.createEvent("Events");
+ event.initEvent(aName, true, true);
+ anElement.dispatchEvent(event);
+ }
+};
+
+var DialogUI = {
+ _dialogs: [],
+ _popup: null,
+
+ init: function() {
+ window.addEventListener("mousedown", this, true);
+ },
+
+ /*******************************************
+ * Modal popups
+ */
+
+ get modals() {
+ return document.getElementsByClassName("modal-block");
+ },
+
+ importModal: function importModal(aParent, aSrc, aArguments) {
+ // load the dialog with a synchronous XHR
+ let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance();
+ xhr.open("GET", aSrc, false);
+ xhr.overrideMimeType("text/xml");
+ xhr.send(null);
+ if (!xhr.responseXML)
+ return null;
+
+ let currentNode;
+ let nodeIterator = xhr.responseXML.createNodeIterator(xhr.responseXML, NodeFilter.SHOW_TEXT, null, false);
+ while (currentNode = nodeIterator.nextNode()) {
+ let trimmed = currentNode.nodeValue.replace(/^\s\s*/, "").replace(/\s\s*$/, "");
+ if (!trimmed.length)
+ currentNode.parentNode.removeChild(currentNode);
+ }
+
+ let doc = xhr.responseXML.documentElement;
+
+ let dialog = null;
+
+ // we need to insert before context-container if we want allow pasting (using
+ // the context menu) into dialogs
+ let contentMenuContainer = document.getElementById("context-container");
+ let parentNode = contentMenuContainer.parentNode;
+
+ // emit DOMWillOpenModalDialog event
+ let event = document.createEvent("Events");
+ event.initEvent("DOMWillOpenModalDialog", true, false);
+ let dispatcher = aParent || getBrowser();
+ dispatcher.dispatchEvent(event);
+
+ // create a full-screen semi-opaque box as a background
+ let back = document.createElement("box");
+ back.setAttribute("class", "modal-block");
+ dialog = back.appendChild(document.importNode(doc, true));
+ parentNode.insertBefore(back, contentMenuContainer);
+
+ dialog.arguments = aArguments;
+ dialog.parent = aParent;
+ return dialog;
+ },
+
+ /*******************************************
+ * Dialogs
+ */
+
+ get activeDialog() {
+ // Return the topmost dialog
+ if (this._dialogs.length)
+ return this._dialogs[this._dialogs.length - 1];
+ return null;
+ },
+
+ closeAllDialogs: function closeAllDialogs() {
+ while (this.activeDialog)
+ this.activeDialog.close();
+ },
+
+ pushDialog: function pushDialog(aDialog) {
+ // If we have a dialog push it on the stack and set the attr for CSS
+ if (aDialog) {
+ this._dialogs.push(aDialog);
+ Elements.contentShowing.setAttribute("disabled", "true");
+ }
+ },
+
+ popDialog: function popDialog() {
+ if (this._dialogs.length)
+ this._dialogs.pop();
+
+ // If no more dialogs are being displayed, remove the attr for CSS
+ if (!this._dialogs.length)
+ Elements.contentShowing.removeAttribute("disabled");
+ },
+
+ /*******************************************
+ * Popups
+ */
+
+ pushPopup: function pushPopup(aPanel, aElements, aParent) {
+ this._hidePopup();
+ this._popup = { "panel": aPanel,
+ "elements": (aElements instanceof Array) ? aElements : [aElements] };
+ this._dispatchPopupChanged(true);
+ },
+
+ popPopup: function popPopup(aPanel) {
+ if (!this._popup || aPanel != this._popup.panel)
+ return;
+ this._popup = null;
+ this._dispatchPopupChanged(false);
+ },
+
+ _hidePopup: function _hidePopup() {
+ if (!this._popup)
+ return;
+ let panel = this._popup.panel;
+ if (panel.hide)
+ panel.hide();
+ },
+
+ /*******************************************
+ * Events
+ */
+
+ handleEvent: function (aEvent) {
+ switch (aEvent.type) {
+ case "mousedown":
+ if (!this._isEventInsidePopup(aEvent))
+ this._hidePopup();
+ break;
+ default:
+ break;
+ }
+ },
+
+ _dispatchPopupChanged: function _dispatchPopupChanged(aVisible) {
+ let event = document.createEvent("UIEvents");
+ event.initUIEvent("PopupChanged", true, true, window, aVisible);
+ event.popup = this._popup;
+ Elements.stack.dispatchEvent(event);
+ },
+
+ _isEventInsidePopup: function _isEventInsidePopup(aEvent) {
+ if (!this._popup)
+ return false;
+ let elements = this._popup.elements;
+ let targetNode = aEvent.target;
+ while (targetNode && elements.indexOf(targetNode) == -1) {
+ if (targetNode instanceof Element && targetNode.hasAttribute("for"))
+ targetNode = document.getElementById(targetNode.getAttribute("for"));
+ else
+ targetNode = targetNode.parentNode;
+ }
+ return targetNode ? true : false;
+ }
+};
+
+/**
+ * Manage the contents of the Windows 8 "Settings" charm.
+ */
+var SettingsCharm = {
+ _entries: new Map(),
+ _nextId: 0,
+
+ /**
+ * Add a new item to the "Settings" menu in the Windows 8 charms.
+ * @param aEntry Object with a "label" property (string that will appear in the UI)
+ * and an "onselected" property (function to be called when the user chooses this entry)
+ */
+ addEntry: function addEntry(aEntry) {
+ let id = MetroUtils.addSettingsPanelEntry(aEntry.label);
+ this._entries.set(id, aEntry);
+ },
+
+ init: function SettingsCharm_init() {
+ Services.obs.addObserver(this, "metro-settings-entry-selected", false);
+
+ // Options
+ this.addEntry({
+ label: Strings.browser.GetStringFromName("optionsCharm"),
+ onselected: function() Elements.prefsFlyout.show()
+ });
+ // About
+ this.addEntry({
+ label: Strings.browser.GetStringFromName("aboutCharm1"),
+ onselected: function() Elements.aboutFlyout.show()
+ });
+ // Help
+ this.addEntry({
+ label: Strings.browser.GetStringFromName("helpOnlineCharm"),
+ onselected: function() {
+ let url = Services.urlFormatter.formatURLPref("app.support.baseURL");
+ BrowserUI.newTab(url, Browser.selectedTab);
+ }
+ });
+ },
+
+ observe: function SettingsCharm_observe(aSubject, aTopic, aData) {
+ if (aTopic == "metro-settings-entry-selected") {
+ let entry = this._entries.get(parseInt(aData, 10));
+ if (entry)
+ entry.onselected();
+ }
+ },
+
+ uninit: function SettingsCharm_uninit() {
+ Services.obs.removeObserver(this, "metro-settings-entry-selected");
+ }
+};
diff --git a/browser/metro/base/content/browser.css b/browser/metro/base/content/browser.css
new file mode 100644
index 000000000000..7632141cb3a6
--- /dev/null
+++ b/browser/metro/base/content/browser.css
@@ -0,0 +1,221 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+browser[remote="false"] {
+ -moz-binding: url("chrome://browser/content/bindings/browser.xml#local-browser");
+}
+
+browser[remote="true"] {
+ -moz-binding: url("chrome://browser/content/bindings/browser.xml#remote-browser");
+}
+
+#content-navigator {
+ -moz-binding: url("chrome://browser/content/bindings/bindings.xml#content-navigator");
+}
+
+#tabs {
+ -moz-binding: url("chrome://browser/content/bindings/tabs.xml#tablist");
+}
+
+documenttab {
+ -moz-binding: url("chrome://browser/content/bindings/tabs.xml#documenttab");
+}
+
+appbar {
+ -moz-binding: url('chrome://browser/content/bindings/appbar.xml#appbarBinding');
+}
+
+flyoutpanel {
+ -moz-binding: url('chrome://browser/content/bindings/flyoutpanel.xml#flyoutpanelBinding');
+}
+
+settings {
+ -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#settings");
+}
+
+setting {
+ display: none;
+}
+
+setting[type="bool"] {
+ display: -moz-box;
+ -moz-binding: url("chrome://browser/content/bindings/toggleswitch.xml#setting-fulltoggle-bool");
+}
+
+setting[type="bool"][localized="true"] {
+ display: -moz-box;
+ -moz-binding: url("chrome://browser/content/bindings/toggleswitch.xml#setting-fulltoggle-localized-bool");
+}
+
+setting[type="boolint"] {
+ display: -moz-box;
+ -moz-binding: url("chrome://browser/content/bindings/toggleswitch.xml#setting-fulltoggle-boolint");
+}
+
+setting[type="integer"] {
+ display: -moz-box;
+ -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-integer");
+}
+
+setting[type="control"] {
+ display: -moz-box;
+ -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-control");
+}
+
+setting[type="string"] {
+ display: -moz-box;
+ -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-string");
+}
+
+setting[type="color"] {
+ display: -moz-box;
+ -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-color");
+}
+
+setting[type="file"],
+setting[type="directory"] {
+ display: -moz-box;
+ -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-path");
+}
+
+setting[type="radio"],
+setting[type="menulist"] {
+ display: -moz-box;
+ -moz-binding: url("chrome://mozapps/content/extensions/setting.xml#setting-multi");
+}
+
+#selection-overlay {
+ -moz-binding: url("chrome://browser/content/bindings/selectionoverlay.xml#selection-binding");
+}
+
+#urlbar-edit {
+ -moz-binding: url("chrome://browser/content/bindings/autocomplete.xml#autocomplete");
+}
+
+#start-autocomplete {
+ -moz-binding: url("chrome://browser/content/bindings/autocomplete.xml#autocomplete-popup");
+}
+
+richgrid {
+ -moz-binding: url("chrome://browser/content/bindings/grid.xml#richgrid");
+}
+
+richgriditem {
+ -moz-binding: url("chrome://browser/content/bindings/grid.xml#richgrid-item");
+}
+
+placeitem {
+ -moz-binding: url("chrome://browser/content/bindings/bindings.xml#place-item");
+ background-color: transparent;
+}
+
+placeitem[type="folder"] {
+ -moz-binding: url("chrome://browser/content/bindings/bindings.xml#place-folder");
+}
+
+placelabel {
+ -moz-binding: url("chrome://browser/content/bindings/bindings.xml#place-label");
+}
+
+radio {
+ -moz-binding: url("chrome://global/content/bindings/radio.xml#radio");
+}
+
+checkbox.toggleswitch {
+ -moz-binding: url("chrome://browser/content/bindings/toggleswitch.xml#checkbox-toggleswitch");
+}
+
+menulist {
+ -moz-binding: url("chrome://browser/content/bindings/bindings.xml#menulist");
+}
+
+.chrome-select-option {
+ -moz-binding: url("chrome://browser/content/bindings/bindings.xml#chrome-select-option");
+}
+
+/* richlist defaults ------------------------------------------------------- */
+richlistbox[batch] {
+ -moz-binding: url("chrome://browser/content/bindings/bindings.xml#richlistbox-batch");
+}
+
+richlistbox[bindingType="contextmenu"] {
+ -moz-binding: url("chrome://browser/content/bindings/bindings.xml#richlistbox-contextmenu");
+}
+
+richlistitem {
+ -moz-binding: url("chrome://browser/content/bindings/bindings.xml#richlistitem");
+}
+
+richgriditem[typeName="download"][state="-1"] {
+ -moz-binding: url("chrome://browser/content/bindings/downloads.xml#download-not-started");
+}
+
+richgriditem[typeName="download"] {
+ -moz-binding: url("chrome://browser/content/bindings/downloads.xml#download-downloading");
+}
+
+richgriditem[typeName="download"][state="1"] {
+ -moz-binding: url("chrome://browser/content/bindings/downloads.xml#download-done");
+}
+
+richgriditem[typeName="download"][state="2"],
+richgriditem[typeName="download"][state="3"] {
+ -moz-binding: url("chrome://browser/content/bindings/downloads.xml#download-retry");
+}
+
+richgriditem[typeName="download"][state="4"] {
+ -moz-binding: url("chrome://browser/content/bindings/downloads.xml#download-paused");
+}
+
+richlistitem[type="error"],
+richlistitem[type="warning"] {
+ -moz-binding: url("chrome://browser/content/bindings/console.xml#error");
+}
+
+richlistitem[type="message"]{
+ -moz-binding: url("chrome://browser/content/bindings/console.xml#message");
+}
+
+dialog {
+ -moz-binding: url("chrome://browser/content/bindings/dialog.xml#dialog");
+}
+
+/* Do not allow these to inherit from the toolkit binding */
+dialog.content-dialog {
+ -moz-binding: none;
+}
+
+pageaction {
+ -moz-binding: url("chrome://browser/content/bindings/pageaction.xml#pageaction");
+}
+
+arrowbox {
+ -moz-binding: url("chrome://browser/content/bindings/arrowbox.xml#arrowbox");
+}
+
+/* Disable context menus in textboxes */
+.textbox-input-box,
+.textbox-input-box[spellcheck="true"] {
+ -moz-binding: url("chrome://browser/content/bindings/bindings.xml#input-box");
+}
+
+textbox {
+ -moz-binding: url("chrome://browser/content/bindings/bindings.xml#textbox");
+}
+
+textbox[multiline="true"] {
+ -moz-binding: url("chrome://browser/content/bindings/bindings.xml#textarea");
+}
+
+textbox[type="timed"] {
+ -moz-binding: url("chrome://browser/content/bindings/bindings.xml#timed-textbox");
+}
+
+textbox[type="search"] {
+ -moz-binding: url("chrome://browser/content/bindings/bindings.xml#search-textbox");
+}
+
+textbox[type="number"] {
+ -moz-binding: url("chrome://browser/content/bindings/bindings.xml#numberbox");
+}
diff --git a/browser/metro/base/content/browser.js b/browser/metro/base/content/browser.js
new file mode 100644
index 000000000000..e2b9fe2b2ff9
--- /dev/null
+++ b/browser/metro/base/content/browser.js
@@ -0,0 +1,1986 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+let Cc = Components.classes;
+let Ci = Components.interfaces;
+let Cu = Components.utils;
+let Cr = Components.results;
+
+const kBrowserViewZoomLevelPrecision = 10000;
+
+// allow panning after this timeout on pages with registered touch listeners
+const kTouchTimeout = 300;
+const kSetInactiveStateTimeout = 100;
+
+const kDefaultMetadata = { autoSize: false, allowZoom: true, autoScale: true };
+
+// Override sizeToContent in the main window. It breaks things (bug 565887)
+window.sizeToContent = function() {
+ Cu.reportError("window.sizeToContent is not allowed in this window");
+}
+
+function getBrowser() {
+ return Browser.selectedBrowser;
+}
+
+var Browser = {
+ _debugEvents: false,
+ _tabs: [],
+ _selectedTab: null,
+ _tabId: 0,
+ windowUtils: window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils),
+
+ get defaultBrowserWidth() {
+ return window.innerWidth;
+ },
+
+ startup: function startup() {
+ var self = this;
+
+ try {
+ messageManager.loadFrameScript("chrome://browser/content/Util.js", true);
+ messageManager.loadFrameScript("chrome://browser/content/contenthandlers/Content.js", true);
+ messageManager.loadFrameScript("chrome://browser/content/contenthandlers/FormHelper.js", true);
+ messageManager.loadFrameScript("chrome://browser/content/contenthandlers/SelectionHandler.js", true);
+ messageManager.loadFrameScript("chrome://browser/content/contenthandlers/ContextMenuHandler.js", true);
+ messageManager.loadFrameScript("chrome://browser/content/contenthandlers/FindHandler.js", true);
+ // XXX Viewport resizing disabled because of bug 766142
+ //messageManager.loadFrameScript("chrome://browser/content/contenthandlers/ViewportHandler.js", true);
+ messageManager.loadFrameScript("chrome://browser/content/contenthandlers/ConsoleAPIObserver.js", true);
+ //messageManager.loadFrameScript("chrome://browser/content/contenthandlers/PluginCTPHandler.js", true);
+ } catch (e) {
+ // XXX whatever is calling startup needs to dump errors!
+ dump("###########" + e + "\n");
+ }
+
+ /* handles dispatching clicks on browser into clicks in content or zooms */
+ Elements.browsers.customDragger = new Browser.MainDragger();
+
+ /* handles web progress management for open browsers */
+ Elements.browsers.webProgress = WebProgress.init();
+
+ // Call InputSourceHelper first so global listeners get called before
+ // we start processing input in TouchModule.
+ InputSourceHelper.init();
+
+ TouchModule.init();
+ ScrollwheelModule.init(Elements.browsers);
+ GestureModule.init();
+ BrowserTouchHandler.init();
+
+ // Warning, total hack ahead. All of the real-browser related scrolling code
+ // lies in a pretend scrollbox here. Let's not land this as-is. Maybe it's time
+ // to redo all the dragging code.
+ this.contentScrollbox = Elements.browsers;
+ this.contentScrollboxScroller = {
+ scrollBy: function(aDx, aDy) {
+ let view = getBrowser().getRootView();
+ view.scrollBy(aDx, aDy);
+ },
+
+ scrollTo: function(aX, aY) {
+ let view = getBrowser().getRootView();
+ view.scrollTo(aX, aY);
+ },
+
+ getPosition: function(aScrollX, aScrollY) {
+ let view = getBrowser().getRootView();
+ let scroll = view.getPosition();
+ aScrollX.value = scroll.x;
+ aScrollY.value = scroll.y;
+ }
+ };
+
+ ContentAreaObserver.init();
+
+ function fullscreenHandler() {
+ if (!window.fullScreen)
+ Elements.toolbar.setAttribute("fullscreen", "true");
+ else
+ Elements.toolbar.removeAttribute("fullscreen");
+ }
+ window.addEventListener("fullscreen", fullscreenHandler, false);
+
+ BrowserUI.init();
+
+ window.controllers.appendController(this);
+ window.controllers.appendController(BrowserUI);
+
+ let os = Services.obs;
+ os.addObserver(SessionHistoryObserver, "browser:purge-session-history", false);
+ os.addObserver(ActivityObserver, "application-background", false);
+ os.addObserver(ActivityObserver, "application-foreground", false);
+ os.addObserver(ActivityObserver, "system-active", false);
+ os.addObserver(ActivityObserver, "system-idle", false);
+ os.addObserver(ActivityObserver, "system-display-on", false);
+ os.addObserver(ActivityObserver, "system-display-off", false);
+
+ window.QueryInterface(Ci.nsIDOMChromeWindow).browserDOMWindow = new nsBrowserAccess();
+
+ Elements.browsers.addEventListener("DOMUpdatePageReport", PopupBlockerObserver.onUpdatePageReport, false);
+
+ // Make sure we're online before attempting to load
+ Util.forceOnline();
+
+ // If this is an intial window launch the commandline handler passes us the default
+ // page as an argument. commandURL _should_ never be empty, but we protect against it
+ // below. However, we delay trying to get the fallback homepage until we really need it.
+ let commandURL = null;
+ if (window.arguments && window.arguments[0])
+ commandURL = window.arguments[0];
+
+ // Should we restore the previous session (crash or some other event)
+ let ss = Cc["@mozilla.org/browser/sessionstore;1"].getService(Ci.nsISessionStore);
+ if (ss.shouldRestore() || Services.prefs.getBoolPref("browser.startup.sessionRestore")) {
+ let bringFront = false;
+ // First open any commandline URLs, except the homepage
+ if (commandURL && commandURL != this.getHomePage()) {
+ this.addTab(commandURL, true);
+ } else {
+ bringFront = true;
+ // Initial window resizes call functions that assume a tab is in the tab list
+ // and restored tabs are added too late. We add a dummy to to satisfy the resize
+ // code and then remove the dummy after the session has been restored.
+ let dummy = this.addTab("about:blank", true);
+ let dummyCleanup = {
+ observe: function(aSubject, aTopic, aData) {
+ Services.obs.removeObserver(dummyCleanup, "sessionstore-windows-restored");
+ if (aData == "fail")
+ Browser.addTab(commandURL || Browser.getHomePage(), true);
+ dummy.chromeTab.ignoreUndo = true;
+ Browser.closeTab(dummy, { forceClose: true });
+ }
+ };
+ Services.obs.addObserver(dummyCleanup, "sessionstore-windows-restored", false);
+ }
+ ss.restoreLastSession(bringFront);
+ } else {
+ this.addTab(commandURL || this.getHomePage(), true);
+ }
+
+ messageManager.addMessageListener("DOMLinkAdded", this);
+ messageManager.addMessageListener("MozScrolledAreaChanged", this);
+ messageManager.addMessageListener("Browser:ViewportMetadata", this);
+ messageManager.addMessageListener("Browser:FormSubmit", this);
+ messageManager.addMessageListener("Browser:ZoomToPoint:Return", this);
+ messageManager.addMessageListener("Browser:CanUnload:Return", this);
+ messageManager.addMessageListener("scroll", this);
+ messageManager.addMessageListener("Browser:CertException", this);
+ messageManager.addMessageListener("Browser:BlockedSite", this);
+ messageManager.addMessageListener("Browser:ErrorPage", this);
+ messageManager.addMessageListener("Browser:TapOnSelection", this);
+ messageManager.addMessageListener("Browser:PluginClickToPlayClicked", this);
+
+ // Let everyone know what kind of mouse input we are
+ // starting with:
+ InputSourceHelper.fireUpdate();
+
+ // Broadcast a UIReady message so add-ons know we are finished with startup
+ let event = document.createEvent("Events");
+ event.initEvent("UIReady", true, false);
+ window.dispatchEvent(event);
+ },
+
+ quit: function quit() {
+ // NOTE: onclose seems to be called only when using OS chrome to close a window,
+ // so we need to handle the Browser.closing check ourselves.
+ if (this.closing()) {
+ window.QueryInterface(Ci.nsIDOMChromeWindow).minimize();
+ window.close();
+ }
+ },
+
+ _waitingToClose: false,
+ closing: function closing() {
+ // If we are already waiting for the close prompt, don't show another
+ if (this._waitingToClose)
+ return false;
+
+ // Prompt if we have multiple tabs before closing window
+ let numTabs = this._tabs.length;
+ if (numTabs > 1) {
+ let shouldPrompt = Services.prefs.getBoolPref("browser.tabs.warnOnClose");
+ if (shouldPrompt) {
+ let prompt = Services.prompt;
+
+ // Default to true: if it were false, we wouldn't get this far
+ let warnOnClose = { value: true };
+
+ let messageBase = Strings.browser.GetStringFromName("tabs.closeWarning");
+ let message = PluralForm.get(numTabs, messageBase).replace("#1", numTabs);
+
+ let title = Strings.browser.GetStringFromName("tabs.closeWarningTitle");
+ let closeText = Strings.browser.GetStringFromName("tabs.closeButton");
+ let checkText = Strings.browser.GetStringFromName("tabs.closeWarningPromptMe");
+ let buttons = (prompt.BUTTON_TITLE_IS_STRING * prompt.BUTTON_POS_0) +
+ (prompt.BUTTON_TITLE_CANCEL * prompt.BUTTON_POS_1);
+
+ this._waitingToClose = true;
+ let pressed = prompt.confirmEx(window, title, message, buttons, closeText, null, null, checkText, warnOnClose);
+ this._waitingToClose = false;
+
+ // Don't set the pref unless they press OK and it's false
+ let reallyClose = (pressed == 0);
+ if (reallyClose && !warnOnClose.value)
+ Services.prefs.setBoolPref("browser.tabs.warnOnClose", false);
+
+ // If we don't want to close, return now. If we are closing, continue with other housekeeping.
+ if (!reallyClose)
+ return false;
+ }
+ }
+
+ // Figure out if there's at least one other browser window around.
+ let lastBrowser = true;
+ let e = Services.wm.getEnumerator("navigator:browser");
+ while (e.hasMoreElements() && lastBrowser) {
+ let win = e.getNext();
+ if (win != window)
+ lastBrowser = false;
+ }
+ if (!lastBrowser)
+ return true;
+
+ // Let everyone know we are closing the last browser window
+ let closingCancelled = Cc["@mozilla.org/supports-PRBool;1"].createInstance(Ci.nsISupportsPRBool);
+ Services.obs.notifyObservers(closingCancelled, "browser-lastwindow-close-requested", null);
+ if (closingCancelled.data)
+ return false;
+
+ Services.obs.notifyObservers(null, "browser-lastwindow-close-granted", null);
+ return true;
+ },
+
+ shutdown: function shutdown() {
+ BrowserUI.uninit();
+ ContentAreaObserver.uninit();
+
+ messageManager.removeMessageListener("MozScrolledAreaChanged", this);
+ messageManager.removeMessageListener("Browser:ViewportMetadata", this);
+ messageManager.removeMessageListener("Browser:FormSubmit", this);
+ messageManager.removeMessageListener("Browser:ZoomToPoint:Return", this);
+ messageManager.removeMessageListener("scroll", this);
+ messageManager.removeMessageListener("Browser:CertException", this);
+ messageManager.removeMessageListener("Browser:BlockedSite", this);
+ messageManager.removeMessageListener("Browser:ErrorPage", this);
+ messageManager.removeMessageListener("Browser:PluginClickToPlayClicked", this);
+ messageManager.removeMessageListener("Browser:TapOnSelection", this);
+
+ var os = Services.obs;
+ os.removeObserver(SessionHistoryObserver, "browser:purge-session-history");
+ os.removeObserver(ActivityObserver, "application-background", false);
+ os.removeObserver(ActivityObserver, "application-foreground", false);
+ os.removeObserver(ActivityObserver, "system-active", false);
+ os.removeObserver(ActivityObserver, "system-idle", false);
+ os.removeObserver(ActivityObserver, "system-display-on", false);
+ os.removeObserver(ActivityObserver, "system-display-off", false);
+
+ window.controllers.removeController(this);
+ window.controllers.removeController(BrowserUI);
+ },
+
+ getHomePage: function getHomePage(aOptions) {
+ aOptions = aOptions || { useDefault: false };
+
+ let url = "about:start";
+ try {
+ let prefs = aOptions.useDefault ? Services.prefs.getDefaultBranch(null) : Services.prefs;
+ url = prefs.getComplexValue("browser.startup.homepage", Ci.nsIPrefLocalizedString).data;
+ }
+ catch(e) { }
+
+ return url;
+ },
+
+ get browsers() {
+ return this._tabs.map(function(tab) { return tab.browser; });
+ },
+
+ /**
+ * Load a URI in the current tab, or a new tab if necessary.
+ * @param aURI String
+ * @param aParams Object with optional properties that will be passed to loadURIWithFlags:
+ * flags, referrerURI, charset, postData.
+ */
+ loadURI: function loadURI(aURI, aParams) {
+ let browser = this.selectedBrowser;
+
+ // We need to keep about: pages opening in new "local" tabs. We also want to spawn
+ // new "remote" tabs if opening web pages from a "local" about: page.
+ dump("loadURI=" + aURI + "\ncurrentURI=" + browser.currentURI.spec + "\n");
+
+ let params = aParams || {};
+ try {
+ let flags = params.flags || Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+ let postData = ("postData" in params && params.postData) ? params.postData.value : null;
+ let referrerURI = "referrerURI" in params ? params.referrerURI : null;
+ let charset = "charset" in params ? params.charset : null;
+ dump("loading tab: " + aURI + "\n");
+ browser.loadURIWithFlags(aURI, flags, referrerURI, charset, postData);
+ } catch(e) {
+ dump("Error: " + e + "\n");
+ }
+ },
+
+ /**
+ * Determine if the given URL is a shortcut/keyword and, if so, expand it
+ * @param aURL String
+ * @param aPostDataRef Out param contains any required post data for a search
+ * @returns the expanded shortcut, or the original URL if not a shortcut
+ */
+ getShortcutOrURI: function getShortcutOrURI(aURL, aPostDataRef) {
+ let shortcutURL = null;
+ let keyword = aURL;
+ let param = "";
+
+ let offset = aURL.indexOf(" ");
+ if (offset > 0) {
+ keyword = aURL.substr(0, offset);
+ param = aURL.substr(offset + 1);
+ }
+
+ if (!aPostDataRef)
+ aPostDataRef = {};
+
+ let engine = Services.search.getEngineByAlias(keyword);
+ if (engine) {
+ let submission = engine.getSubmission(param);
+ aPostDataRef.value = submission.postData;
+ return submission.uri.spec;
+ }
+
+ try {
+ [shortcutURL, aPostDataRef.value] = PlacesUtils.getURLAndPostDataForKeyword(keyword);
+ } catch (e) {}
+
+ if (!shortcutURL)
+ return aURL;
+
+ let postData = "";
+ if (aPostDataRef.value)
+ postData = unescape(aPostDataRef.value);
+
+ if (/%s/i.test(shortcutURL) || /%s/i.test(postData)) {
+ let charset = "";
+ const re = /^(.*)\&mozcharset=([a-zA-Z][_\-a-zA-Z0-9]+)\s*$/;
+ let matches = shortcutURL.match(re);
+ if (matches)
+ [, shortcutURL, charset] = matches;
+ else {
+ // Try to get the saved character-set.
+ try {
+ // makeURI throws if URI is invalid.
+ // Will return an empty string if character-set is not found.
+ charset = PlacesUtils.history.getCharsetForURI(Util.makeURI(shortcutURL));
+ } catch (e) { dump("--- error " + e + "\n"); }
+ }
+
+ let encodedParam = "";
+ if (charset)
+ encodedParam = escape(convertFromUnicode(charset, param));
+ else // Default charset is UTF-8
+ encodedParam = encodeURIComponent(param);
+
+ shortcutURL = shortcutURL.replace(/%s/g, encodedParam).replace(/%S/g, param);
+
+ if (/%s/i.test(postData)) // POST keyword
+ aPostDataRef.value = getPostDataStream(postData, param, encodedParam, "application/x-www-form-urlencoded");
+ } else if (param) {
+ // This keyword doesn't take a parameter, but one was provided. Just return
+ // the original URL.
+ aPostDataRef.value = null;
+
+ return aURL;
+ }
+
+ return shortcutURL;
+ },
+
+ /**
+ * Return the currently active object
+ */
+ get selectedBrowser() {
+ return (this._selectedTab && this._selectedTab.browser);
+ },
+
+ get tabs() {
+ return this._tabs;
+ },
+
+ getTabForBrowser: function getTabForBrowser(aBrowser) {
+ let tabs = this._tabs;
+ for (let i = 0; i < tabs.length; i++) {
+ if (tabs[i].browser == aBrowser)
+ return tabs[i];
+ }
+ return null;
+ },
+
+ getBrowserForWindowId: function getBrowserForWindowId(aWindowId) {
+ for (let i = 0; i < this.browsers.length; i++) {
+ if (this.browsers[i].contentWindowId == aWindowId)
+ return this.browsers[i];
+ }
+ return null;
+ },
+
+ getTabAtIndex: function getTabAtIndex(index) {
+ if (index >= this._tabs.length || index < 0)
+ return null;
+ return this._tabs[index];
+ },
+
+ getTabFromChrome: function getTabFromChrome(chromeTab) {
+ for (var t = 0; t < this._tabs.length; t++) {
+ if (this._tabs[t].chromeTab == chromeTab)
+ return this._tabs[t];
+ }
+ return null;
+ },
+
+ createTabId: function createTabId() {
+ return this._tabId++;
+ },
+
+ addTab: function browser_addTab(aURI, aBringFront, aOwner, aParams) {
+ let params = aParams || {};
+ let newTab = new Tab(aURI, params);
+ newTab.owner = aOwner || null;
+ this._tabs.push(newTab);
+
+ if (aBringFront)
+ this.selectedTab = newTab;
+
+ let getAttention = ("getAttention" in params ? params.getAttention : !aBringFront);
+ let event = document.createEvent("UIEvents");
+ event.initUIEvent("TabOpen", true, false, window, getAttention);
+ newTab.chromeTab.dispatchEvent(event);
+ newTab.browser.messageManager.sendAsyncMessage("Browser:TabOpen");
+
+ return newTab;
+ },
+
+ closeTab: function closeTab(aTab, aOptions) {
+ let tab = aTab instanceof XULElement ? this.getTabFromChrome(aTab) : aTab;
+ if (!tab || !this._getNextTab(tab))
+ return;
+
+ if (aOptions && "forceClose" in aOptions && aOptions.forceClose) {
+ this._doCloseTab(aTab);
+ return;
+ }
+
+ tab.browser.messageManager.sendAsyncMessage("Browser:CanUnload", {});
+ },
+
+ _doCloseTab: function _doCloseTab(aTab) {
+ let nextTab = this._getNextTab(aTab);
+ if (!nextTab)
+ return;
+
+ // Tabs owned by the closed tab are now orphaned.
+ this._tabs.forEach(function(item, index, array) {
+ if (item.owner == aTab)
+ item.owner = null;
+ });
+
+ let event = document.createEvent("Events");
+ event.initEvent("TabClose", true, false);
+ aTab.chromeTab.dispatchEvent(event);
+ aTab.browser.messageManager.sendAsyncMessage("Browser:TabClose");
+
+ let container = aTab.chromeTab.parentNode;
+ aTab.destroy();
+ this._tabs.splice(this._tabs.indexOf(aTab), 1);
+
+ this.selectedTab = nextTab;
+
+ event = document.createEvent("Events");
+ event.initEvent("TabRemove", true, false);
+ container.dispatchEvent(event);
+ },
+
+ _getNextTab: function _getNextTab(aTab) {
+ let tabIndex = this._tabs.indexOf(aTab);
+ if (tabIndex == -1)
+ return null;
+
+ let nextTab = this._selectedTab;
+ if (nextTab == aTab) {
+ nextTab = this.getTabAtIndex(tabIndex + 1) || this.getTabAtIndex(tabIndex - 1);
+
+ // If the next tab is not a sibling, switch back to the parent.
+ if (aTab.owner && nextTab.owner != aTab.owner)
+ nextTab = aTab.owner;
+
+ if (!nextTab)
+ return null;
+ }
+
+ return nextTab;
+ },
+
+ get selectedTab() {
+ return this._selectedTab;
+ },
+
+ set selectedTab(tab) {
+ if (tab instanceof XULElement)
+ tab = this.getTabFromChrome(tab);
+
+ if (!tab)
+ return;
+
+ if (this._selectedTab == tab) {
+ // Deck does not update its selectedIndex when children
+ // are removed. See bug 602708
+ Elements.browsers.selectedPanel = tab.notification;
+ return;
+ }
+
+ let isFirstTab = this._selectedTab == null;
+ let lastTab = this._selectedTab;
+ let oldBrowser = lastTab ? lastTab._browser : null;
+ let browser = tab.browser;
+
+ this._selectedTab = tab;
+
+ if (lastTab)
+ lastTab.active = false;
+
+ if (tab)
+ tab.active = true;
+
+ if (isFirstTab) {
+ // Don't waste time at startup updating the whole UI; just display the URL.
+ BrowserUI._titleChanged(browser);
+ } else {
+ // Update all of our UI to reflect the new tab's location
+ BrowserUI.updateURI();
+ IdentityUI.checkIdentity();
+
+ let event = document.createEvent("Events");
+ event.initEvent("TabSelect", true, false);
+ event.lastTab = lastTab;
+ tab.chromeTab.dispatchEvent(event);
+ }
+
+ tab.lastSelected = Date.now();
+ },
+
+ supportsCommand: function(cmd) {
+ return false;
+ },
+
+ isCommandEnabled: function(cmd) {
+ return false;
+ },
+
+ doCommand: function(cmd) {
+ },
+
+ getNotificationBox: function getNotificationBox(aBrowser) {
+ let browser = aBrowser || this.selectedBrowser;
+ return browser.parentNode;
+ },
+
+ /**
+ * Handle cert exception message from content.
+ */
+ _handleCertException: function _handleCertException(aMessage) {
+ let json = aMessage.json;
+ if (json.action == "leave") {
+ // Get the start page from the *default* pref branch, not the user's
+ let url = Browser.getHomePage({ useDefault: true });
+ this.loadURI(url);
+ } else {
+ // Handle setting an cert exception and reloading the page
+ try {
+ // Add a new SSL exception for this URL
+ let uri = Services.io.newURI(json.url, null, null);
+ let sslExceptions = new SSLExceptions();
+
+ if (json.action == "permanent")
+ sslExceptions.addPermanentException(uri, errorDoc.defaultView);
+ else
+ sslExceptions.addTemporaryException(uri, errorDoc.defaultView);
+ } catch (e) {
+ dump("EXCEPTION handle content command: " + e + "\n" );
+ }
+
+ // Automatically reload after the exception was added
+ aMessage.target.reload();
+ }
+ },
+
+ /**
+ * Handle blocked site message from content.
+ */
+ _handleBlockedSite: function _handleBlockedSite(aMessage) {
+ let formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].getService(Ci.nsIURLFormatter);
+ let json = aMessage.json;
+ switch (json.action) {
+ case "leave": {
+ // Get the start page from the *default* pref branch, not the user's
+ let url = Browser.getHomePage({ useDefault: true });
+ this.loadURI(url);
+ break;
+ }
+ case "report-malware": {
+ // Get the stop badware "why is this blocked" report url, append the current url, and go there.
+ try {
+ let reportURL = formatter.formatURLPref("browser.safebrowsing.malware.reportURL");
+ reportURL += json.url;
+ this.loadURI(reportURL);
+ } catch (e) {
+ Cu.reportError("Couldn't get malware report URL: " + e);
+ }
+ break;
+ }
+ case "report-phishing": {
+ // It's a phishing site, not malware
+ try {
+ let reportURL = formatter.formatURLPref("browser.safebrowsing.warning.infoURL");
+ this.loadURI(reportURL);
+ } catch (e) {
+ Cu.reportError("Couldn't get phishing info URL: " + e);
+ }
+ break;
+ }
+ }
+ },
+
+ pinSite: function browser_pinSite() {
+ // We use a unique ID per URL, so just use an MD5 hash of the URL as the ID.
+ let hasher = Cc["@mozilla.org/security/hash;1"].
+ createInstance(Ci.nsICryptoHash);
+ hasher.init(Ci.nsICryptoHash.MD5);
+ let stringStream = Cc["@mozilla.org/io/string-input-stream;1"].
+ createInstance(Ci.nsIStringInputStream);
+ stringStream.data = Browser.selectedBrowser.currentURI.spec;
+ hasher.updateFromStream(stringStream, -1);
+ let hashASCII = hasher.finish(true);
+
+ // Get a path to our app tile
+ var file = Components.classes["@mozilla.org/file/directory_service;1"].
+ getService(Components.interfaces.nsIProperties).
+ get("CurProcD", Components.interfaces.nsIFile);
+ // Get rid of the current working directory's metro subidr
+ file = file.parent;
+ file.append("tileresources");
+ file.append("VisualElements_logo.png");
+ var ios = Components.classes["@mozilla.org/network/io-service;1"].
+ getService(Components.interfaces.nsIIOService);
+ var uriSpec = ios.newFileURI(file).spec;
+ MetroUtils.pinTileAsync("FFTileID_" + hashASCII,
+ Browser.selectedBrowser.contentTitle, // short name
+ Browser.selectedBrowser.contentTitle, // display name
+ "metrobrowser -url " + Browser.selectedBrowser.currentURI.spec,
+ uriSpec,
+ uriSpec);
+ },
+
+ unpinSite: function browser_unpinSite() {
+ if (!MetroUtils.immersive)
+ return;
+
+ // We use a unique ID per URL, so just use an MD5 hash of the URL as the ID.
+ let hasher = Cc["@mozilla.org/security/hash;1"].
+ createInstance(Ci.nsICryptoHash);
+ hasher.init(Ci.nsICryptoHash.MD5);
+ let stringStream = Cc["@mozilla.org/io/string-input-stream;1"].
+ createInstance(Ci.nsIStringInputStream);
+ stringStream.data = Browser.selectedBrowser.currentURI.spec;
+ hasher.updateFromStream(stringStream, -1);
+ let hashASCII = hasher.finish(true);
+
+ MetroUtils.unpinTileAsync("FFTileID_" + hashASCII);
+ },
+
+ isSitePinned: function browser_isSitePinned() {
+ if (!MetroUtils.immersive)
+ return false;
+
+ // We use a unique ID per URL, so just use an MD5 hash of the URL as the ID.
+ let hasher = Cc["@mozilla.org/security/hash;1"].
+ createInstance(Ci.nsICryptoHash);
+ hasher.init(Ci.nsICryptoHash.MD5);
+ let stringStream = Cc["@mozilla.org/io/string-input-stream;1"].
+ createInstance(Ci.nsIStringInputStream);
+ stringStream.data = Browser.selectedBrowser.currentURI.spec;
+ hasher.updateFromStream(stringStream, -1);
+ let hashASCII = hasher.finish(true);
+
+ return MetroUtils.isTilePinned("FFTileID_" + hashASCII);
+ },
+
+ starSite: function browser_starSite(callback) {
+ let uri = this.selectedBrowser.currentURI;
+ let title = this.selectedBrowser.contentTitle;
+
+ Bookmarks.addForURI(uri, title, callback);
+ },
+
+ unstarSite: function browser_unstarSite(callback) {
+ let uri = this.selectedBrowser.currentURI;
+ Bookmarks.removeForURI(uri, callback);
+ },
+
+ isSiteStarredAsync: function browser_isSiteStarredAsync(callback) {
+ let uri = this.selectedBrowser.currentURI;
+ Bookmarks.isURIBookmarked(uri, callback);
+ },
+
+ /** Zoom one step in (negative) or out (positive). */
+ zoom: function zoom(aDirection) {
+ let tab = this.selectedTab;
+ if (!tab.allowZoom)
+ return;
+
+ let browser = tab.browser;
+ let oldZoomLevel = browser.scale;
+ let zoomLevel = oldZoomLevel;
+
+ let zoomValues = ZoomManager.zoomValues;
+ let i = zoomValues.indexOf(ZoomManager.snap(zoomLevel)) + (aDirection < 0 ? 1 : -1);
+ if (i >= 0 && i < zoomValues.length)
+ zoomLevel = zoomValues[i];
+
+ zoomLevel = tab.clampZoomLevel(zoomLevel);
+
+ let browserRect = browser.getBoundingClientRect();
+ let center = browser.transformClientToBrowser(browserRect.width / 2,
+ browserRect.height / 2);
+ let rect = this._getZoomRectForPoint(center.x, center.y, zoomLevel);
+ AnimatedZoom.animateTo(rect);
+ },
+
+ /** Rect should be in browser coordinates. */
+ _getZoomLevelForRect: function _getZoomLevelForRect(rect) {
+ const margin = 15;
+ return this.selectedTab.clampZoomLevel(ContentAreaObserver.width / (rect.width + margin * 2));
+ },
+
+ /**
+ * Find an appropriate zoom rect for an element bounding rect, if it exists.
+ * @return Rect in viewport coordinates, or null
+ */
+ _getZoomRectForRect: function _getZoomRectForRect(rect, y) {
+ let zoomLevel = this._getZoomLevelForRect(rect);
+ return this._getZoomRectForPoint(rect.center().x, y, zoomLevel);
+ },
+
+ /**
+ * Find a good zoom rectangle for point that is specified in browser coordinates.
+ * @return Rect in viewport coordinates
+ */
+ _getZoomRectForPoint: function _getZoomRectForPoint(x, y, zoomLevel) {
+ let browser = getBrowser();
+ x = x * browser.scale;
+ y = y * browser.scale;
+
+ zoomLevel = Math.min(ZoomManager.MAX, zoomLevel);
+ let oldScale = browser.scale;
+ let zoomRatio = zoomLevel / oldScale;
+ let browserRect = browser.getBoundingClientRect();
+ let newVisW = browserRect.width / zoomRatio, newVisH = browserRect.height / zoomRatio;
+ let result = new Rect(x - newVisW / 2, y - newVisH / 2, newVisW, newVisH);
+
+ // Make sure rectangle doesn't poke out of viewport
+ return result.translateInside(new Rect(0, 0, browser.contentDocumentWidth * oldScale,
+ browser.contentDocumentHeight * oldScale));
+ },
+
+ zoomToPoint: function zoomToPoint(cX, cY, aRect) {
+ let tab = this.selectedTab;
+ if (!tab.allowZoom)
+ return null;
+
+ let zoomRect = null;
+ if (aRect)
+ zoomRect = this._getZoomRectForRect(aRect, cY);
+
+ if (!zoomRect && tab.isDefaultZoomLevel()) {
+ let scale = tab.clampZoomLevel(tab.browser.scale * 2);
+ zoomRect = this._getZoomRectForPoint(cX, cY, scale);
+ }
+
+ if (zoomRect)
+ AnimatedZoom.animateTo(zoomRect);
+
+ return zoomRect;
+ },
+
+ zoomFromPoint: function zoomFromPoint(cX, cY) {
+ let tab = this.selectedTab;
+ if (tab.allowZoom && !tab.isDefaultZoomLevel()) {
+ let zoomLevel = tab.getDefaultZoomLevel();
+ let zoomRect = this._getZoomRectForPoint(cX, cY, zoomLevel);
+ AnimatedZoom.animateTo(zoomRect);
+ }
+ },
+
+ // The device-pixel-to-CSS-px ratio used to adjust meta viewport values.
+ // This is higher on higher-dpi displays, so pages stay about the same physical size.
+ getScaleRatio: function getScaleRatio() {
+ let prefValue = Services.prefs.getIntPref("browser.viewport.scaleRatio");
+ if (prefValue > 0)
+ return prefValue / 100;
+
+ let dpi = Util.displayDPI;
+ if (dpi < 200) // Includes desktop displays, and LDPI and MDPI Android devices
+ return 1;
+ else if (dpi < 300) // Includes Nokia N900, and HDPI Android devices
+ return 1.5;
+
+ // For very high-density displays like the iPhone 4, calculate an integer ratio.
+ return Math.floor(dpi / 150);
+ },
+
+ /**
+ * Convenience function for getting the scrollbox position off of a
+ * scrollBoxObject interface. Returns the actual values instead of the
+ * wrapping objects.
+ *
+ * @param scroller a scrollBoxObject on which to call scroller.getPosition()
+ */
+ getScrollboxPosition: function getScrollboxPosition(scroller) {
+ let x = {};
+ let y = {};
+ scroller.getPosition(x, y);
+ return new Point(x.value, y.value);
+ },
+
+ forceChromeReflow: function forceChromeReflow() {
+ let dummy = getComputedStyle(document.documentElement, "").width;
+ },
+
+ receiveMessage: function receiveMessage(aMessage) {
+ let json = aMessage.json;
+ let browser = aMessage.target;
+
+ switch (aMessage.name) {
+ case "DOMLinkAdded": {
+ // checks for an icon to use for a web app
+ // apple-touch-icon size is 57px and default size is 16px
+ let rel = json.rel.toLowerCase().split(" ");
+ if (rel.indexOf("icon") != -1) {
+ // We use the sizes attribute if available
+ // see http://www.whatwg.org/specs/web-apps/current-work/multipage/links.html#rel-icon
+ let size = 16;
+ if (json.sizes) {
+ let sizes = json.sizes.toLowerCase().split(" ");
+ sizes.forEach(function(item) {
+ if (item != "any") {
+ let [w, h] = item.split("x");
+ size = Math.max(Math.min(w, h), size);
+ }
+ });
+ }
+ if (size > browser.appIcon.size) {
+ browser.appIcon.href = json.href;
+ browser.appIcon.size = size;
+ }
+ }
+ else if ((rel.indexOf("apple-touch-icon") != -1) && (browser.appIcon.size < 57)) {
+ // XXX should we support apple-touch-icon-precomposed ?
+ // see http://developer.apple.com/safari/library/documentation/appleapplications/reference/safariwebcontent/configuringwebapplications/configuringwebapplications.html
+ browser.appIcon.href = json.href;
+ browser.appIcon.size = 57;
+ }
+ break;
+ }
+ case "MozScrolledAreaChanged": {
+ let tab = this.getTabForBrowser(browser);
+ if (tab)
+ tab.scrolledAreaChanged();
+ break;
+ }
+ case "Browser:ViewportMetadata": {
+ let tab = this.getTabForBrowser(browser);
+ // Some browser such as iframes loaded dynamically into the chrome UI
+ // does not have any assigned tab
+ if (tab)
+ tab.updateViewportMetadata(json);
+ break;
+ }
+ case "Browser:FormSubmit":
+ browser.lastLocation = null;
+ break;
+
+ case "Browser:CanUnload:Return": {
+ if (!json.permit)
+ return;
+
+ // Allow a little delay to not close the target tab while processing
+ // a message for this particular tab
+ setTimeout(function(self) {
+ let tab = self.getTabForBrowser(browser);
+ self._doCloseTab(tab);
+ }, 0, this);
+ break;
+ }
+ case "Browser:ZoomToPoint:Return":
+ if (json.zoomTo) {
+ let rect = Rect.fromRect(json.zoomTo);
+ this.zoomToPoint(json.x, json.y, rect);
+ } else {
+ this.zoomFromPoint(json.x, json.y);
+ }
+ break;
+ case "Browser:CertException":
+ this._handleCertException(aMessage);
+ break;
+ case "Browser:BlockedSite":
+ this._handleBlockedSite(aMessage);
+ break;
+ case "Browser:ErrorPage":
+ break;
+ case "Browser:PluginClickToPlayClicked": {
+ // Save off session history
+ let parent = browser.parentNode;
+ let data = browser.__SS_data;
+ if (data.entries.length == 0)
+ return;
+
+ // Remove the browser from the DOM, effectively killing it's content
+ parent.removeChild(browser);
+
+ // Re-create the browser as non-remote, so plugins work
+ browser.setAttribute("remote", "false");
+ parent.appendChild(browser);
+
+ // Reload the content using session history
+ browser.__SS_data = data;
+ let json = {
+ uri: data.entries[data.index - 1].url,
+ flags: null,
+ entries: data.entries,
+ index: data.index
+ };
+ browser.messageManager.sendAsyncMessage("WebNavigation:LoadURI", json);
+ break;
+ }
+
+ case "Browser:TapOnSelection":
+ if (!InputSourceHelper.isPrecise) {
+ if (SelectionHelperUI.isActive()) {
+ SelectionHelperUI.shutdown();
+ }
+ if (SelectionHelperUI.canHandle(aMessage)) {
+ SelectionHelperUI.openEditSession(aMessage);
+ }
+ }
+ break;
+ }
+ },
+
+ onAboutPolicyClick: function() {
+ FlyoutPanelsUI.hide();
+ BrowserUI.newTab(Services.prefs.getCharPref("app.privacyURL"),
+ Browser.selectedTab);
+ }
+
+};
+
+Browser.MainDragger = function MainDragger() {
+ this._horizontalScrollbar = document.getElementById("horizontal-scroller");
+ this._verticalScrollbar = document.getElementById("vertical-scroller");
+ this._scrollScales = { x: 0, y: 0 };
+
+ Elements.browsers.addEventListener("PanBegin", this, false);
+ Elements.browsers.addEventListener("PanFinished", this, false);
+};
+
+Browser.MainDragger.prototype = {
+ isDraggable: function isDraggable(target, scroller) {
+ return { x: true, y: true };
+ },
+
+ dragStart: function dragStart(clientX, clientY, target, scroller) {
+ let browser = getBrowser();
+ let bcr = browser.getBoundingClientRect();
+ this._contentView = browser.getViewAt(clientX - bcr.left, clientY - bcr.top);
+ },
+
+ dragStop: function dragStop(dx, dy, scroller) {
+ if (this._contentView && this._contentView._updateCacheViewport)
+ this._contentView._updateCacheViewport();
+ this._contentView = null;
+ },
+
+ dragMove: function dragMove(dx, dy, scroller, aIsKinetic) {
+ let doffset = new Point(dx, dy);
+
+ this._panContent(doffset);
+
+ if (aIsKinetic && doffset.x != 0)
+ return false;
+
+ this._updateScrollbars();
+
+ return !doffset.equals(dx, dy);
+ },
+
+ handleEvent: function handleEvent(aEvent) {
+ let browser = getBrowser();
+ switch (aEvent.type) {
+ case "PanBegin": {
+ let width = ContentAreaObserver.width, height = ContentAreaObserver.height;
+ let contentWidth = browser.contentDocumentWidth * browser.scale;
+ let contentHeight = browser.contentDocumentHeight * browser.scale;
+
+ // Allow a small margin on both sides to prevent adding scrollbars
+ // on small viewport approximation
+ const ALLOWED_MARGIN = 5;
+ const SCROLL_CORNER_SIZE = 8;
+ this._scrollScales = {
+ x: (width + ALLOWED_MARGIN) < contentWidth ? (width - SCROLL_CORNER_SIZE) / contentWidth : 0,
+ y: (height + ALLOWED_MARGIN) < contentHeight ? (height - SCROLL_CORNER_SIZE) / contentHeight : 0
+ }
+
+ this._showScrollbars();
+ break;
+ }
+ case "PanFinished":
+ this._hideScrollbars();
+
+ // Update the scroll position of the content
+ browser._updateCSSViewport();
+ break;
+ }
+ },
+
+ _panContent: function md_panContent(aOffset) {
+ if (this._contentView && !this._contentView.isRoot()) {
+ this._panContentView(this._contentView, aOffset);
+ // XXX we may need to have "escape borders" for iframe panning
+ // XXX does not deal with scrollables within scrollables
+ }
+ // Do content panning
+ this._panContentView(getBrowser().getRootView(), aOffset);
+ },
+
+ /** Pan scroller by the given amount. Updates doffset with leftovers. */
+ _panContentView: function _panContentView(contentView, doffset) {
+ let pos0 = contentView.getPosition();
+ contentView.scrollBy(doffset.x, doffset.y);
+ let pos1 = contentView.getPosition();
+ doffset.subtract(pos1.x - pos0.x, pos1.y - pos0.y);
+ },
+
+ _updateScrollbars: function _updateScrollbars() {
+ let scaleX = this._scrollScales.x, scaleY = this._scrollScales.y;
+ let contentScroll = Browser.getScrollboxPosition(Browser.contentScrollboxScroller);
+
+ if (scaleX)
+ this._horizontalScrollbar.style.MozTransform =
+ "translateX(" + Math.round(contentScroll.x * scaleX) + "px)";
+
+ if (scaleY) {
+ let y = Math.round(contentScroll.y * scaleY);
+ let x = 0;
+
+ this._verticalScrollbar.style.MozTransform =
+ "translate(" + x + "px," + y + "px)";
+ }
+ },
+
+ _showScrollbars: function _showScrollbars() {
+ this._updateScrollbars();
+ let scaleX = this._scrollScales.x, scaleY = this._scrollScales.y;
+ if (scaleX) {
+ this._horizontalScrollbar.width = ContentAreaObserver.width * scaleX;
+ this._horizontalScrollbar.setAttribute("panning", "true");
+ }
+
+ if (scaleY) {
+ this._verticalScrollbar.height = ContentAreaObserver.height * scaleY;
+ this._verticalScrollbar.setAttribute("panning", "true");
+ }
+ },
+
+ _hideScrollbars: function _hideScrollbars() {
+ this._scrollScales.x = 0, this._scrollScales.y = 0;
+ this._horizontalScrollbar.removeAttribute("panning");
+ this._verticalScrollbar.removeAttribute("panning");
+ this._horizontalScrollbar.style.MozTransform = "";
+ this._verticalScrollbar.style.MozTransform = "";
+ }
+};
+
+
+
+const OPEN_APPTAB = 100; // Hack until we get a real API
+
+function nsBrowserAccess() { }
+
+nsBrowserAccess.prototype = {
+ QueryInterface: function(aIID) {
+ if (aIID.equals(Ci.nsIBrowserDOMWindow) || aIID.equals(Ci.nsISupports))
+ return this;
+ throw Cr.NS_NOINTERFACE;
+ },
+
+ _getBrowser: function _getBrowser(aURI, aOpener, aWhere, aContext) {
+ let isExternal = (aContext == Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL);
+ if (isExternal && aURI && aURI.schemeIs("chrome"))
+ return null;
+
+ let loadflags = isExternal ?
+ Ci.nsIWebNavigation.LOAD_FLAGS_FROM_EXTERNAL :
+ Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+ let location;
+ if (aWhere == Ci.nsIBrowserDOMWindow.OPEN_DEFAULTWINDOW) {
+ switch (aContext) {
+ case Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL :
+ aWhere = Services.prefs.getIntPref("browser.link.open_external");
+ break;
+ default : // OPEN_NEW or an illegal value
+ aWhere = Services.prefs.getIntPref("browser.link.open_newwindow");
+ }
+ }
+
+ let browser;
+ if (aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWWINDOW) {
+ let url = aURI ? aURI.spec : "about:blank";
+ let newWindow = openDialog("chrome://browser/content/browser.xul", "_blank",
+ "all,dialog=no", url, null, null, null);
+ // since newWindow.Browser doesn't exist yet, just return null
+ return null;
+ } else if (aWhere == Ci.nsIBrowserDOMWindow.OPEN_NEWTAB) {
+ let owner = isExternal ? null : Browser.selectedTab;
+ let tab = Browser.addTab("about:blank", true, owner, { getAttention: true });
+ if (isExternal)
+ tab.closeOnExit = true;
+ browser = tab.browser;
+ } else if (aWhere == OPEN_APPTAB) {
+ Browser.tabs.forEach(function(aTab) {
+ if ("appURI" in aTab.browser && aTab.browser.appURI.spec == aURI.spec) {
+ Browser.selectedTab = aTab;
+ browser = aTab.browser;
+ }
+ });
+
+ if (!browser) {
+ // Make a new tab to hold the app
+ let tab = Browser.addTab("about:blank", true, null, { getAttention: true });
+ browser = tab.browser;
+ browser.appURI = aURI;
+ } else {
+ // Just use the existing browser, but return null to keep the system from trying to load the URI again
+ browser = null;
+ }
+ } else { // OPEN_CURRENTWINDOW and illegal values
+ browser = Browser.selectedBrowser;
+ }
+
+ try {
+ let referrer;
+ if (aURI && browser) {
+ if (aOpener) {
+ location = aOpener.location;
+ referrer = Services.io.newURI(location, null, null);
+ }
+ browser.loadURIWithFlags(aURI.spec, loadflags, referrer, null, null);
+ }
+ browser.focus();
+ } catch(e) { }
+
+ // We are loading web content into this window, so make sure content is visible
+ // XXX Can we remove this? It seems to be reproduced in BrowserUI already.
+ BrowserUI.showContent();
+
+ return browser;
+ },
+
+ openURI: function browser_openURI(aURI, aOpener, aWhere, aContext) {
+ let browser = this._getBrowser(aURI, aOpener, aWhere, aContext);
+ return browser ? browser.contentWindow : null;
+ },
+
+ openURIInFrame: function browser_openURIInFrame(aURI, aOpener, aWhere, aContext) {
+ let browser = this._getBrowser(aURI, aOpener, aWhere, aContext);
+ return browser ? browser.QueryInterface(Ci.nsIFrameLoaderOwner) : null;
+ },
+
+ zoom: function browser_zoom(aAmount) {
+ Browser.zoom(aAmount);
+ },
+
+ isTabContentWindow: function(aWindow) {
+ return Browser.browsers.some(function (browser) browser.contentWindow == aWindow);
+ }
+};
+
+/**
+ * Handler for blocked popups, triggered by DOMUpdatePageReport events in browser.xml
+ */
+var PopupBlockerObserver = {
+ onUpdatePageReport: function onUpdatePageReport(aEvent)
+ {
+ var cBrowser = Browser.selectedBrowser;
+ if (aEvent.originalTarget != cBrowser)
+ return;
+
+ if (!cBrowser.pageReport)
+ return;
+
+ let result = Services.perms.testExactPermission(Browser.selectedBrowser.currentURI, "popup");
+ if (result == Ci.nsIPermissionManager.DENY_ACTION)
+ return;
+
+ // Only show the notification again if we've not already shown it. Since
+ // notifications are per-browser, we don't need to worry about re-adding
+ // it.
+ if (!cBrowser.pageReport.reported) {
+ if (Services.prefs.getBoolPref("privacy.popups.showBrowserMessage")) {
+ var brandShortName = Strings.brand.GetStringFromName("brandShortName");
+ var message;
+ var popupCount = cBrowser.pageReport.length;
+
+ let strings = Strings.browser;
+ if (popupCount > 1)
+ message = strings.formatStringFromName("popupWarningMultiple", [brandShortName, popupCount], 2);
+ else
+ message = strings.formatStringFromName("popupWarning", [brandShortName], 1);
+
+ var notificationBox = Browser.getNotificationBox();
+ var notification = notificationBox.getNotificationWithValue("popup-blocked");
+ if (notification) {
+ notification.label = message;
+ }
+ else {
+ var buttons = [
+ {
+ label: strings.GetStringFromName("popupButtonAllowOnce"),
+ accessKey: null,
+ callback: function() { PopupBlockerObserver.showPopupsForSite(); }
+ },
+ {
+ label: strings.GetStringFromName("popupButtonAlwaysAllow2"),
+ accessKey: null,
+ callback: function() { PopupBlockerObserver.allowPopupsForSite(true); }
+ },
+ {
+ label: strings.GetStringFromName("popupButtonNeverWarn2"),
+ accessKey: null,
+ callback: function() { PopupBlockerObserver.allowPopupsForSite(false); }
+ }
+ ];
+
+ const priority = notificationBox.PRIORITY_WARNING_MEDIUM;
+ notificationBox.appendNotification(message, "popup-blocked",
+ "",
+ priority, buttons);
+ }
+ }
+ // Record the fact that we've reported this blocked popup, so we don't
+ // show it again.
+ cBrowser.pageReport.reported = true;
+ }
+ },
+
+ allowPopupsForSite: function allowPopupsForSite(aAllow) {
+ var currentURI = Browser.selectedBrowser.currentURI;
+ Services.perms.add(currentURI, "popup", aAllow
+ ? Ci.nsIPermissionManager.ALLOW_ACTION
+ : Ci.nsIPermissionManager.DENY_ACTION);
+
+ Browser.getNotificationBox().removeCurrentNotification();
+ },
+
+ showPopupsForSite: function showPopupsForSite() {
+ let uri = Browser.selectedBrowser.currentURI;
+ let pageReport = Browser.selectedBrowser.pageReport;
+ if (pageReport) {
+ for (let i = 0; i < pageReport.length; ++i) {
+ var popupURIspec = pageReport[i].popupWindowURI.spec;
+
+ // Sometimes the popup URI that we get back from the pageReport
+ // isn't useful (for instance, netscape.com's popup URI ends up
+ // being "http://www.netscape.com", which isn't really the URI of
+ // the popup they're trying to show). This isn't going to be
+ // useful to the user, so we won't create a menu item for it.
+ if (popupURIspec == "" || !Util.isURLMemorable(popupURIspec) || popupURIspec == uri.spec)
+ continue;
+
+ let popupFeatures = pageReport[i].popupWindowFeatures;
+ let popupName = pageReport[i].popupWindowName;
+
+ Browser.addTab(popupURIspec, false, Browser.selectedTab);
+ }
+ }
+ }
+};
+
+var SessionHistoryObserver = {
+ observe: function sho_observe(aSubject, aTopic, aData) {
+ if (aTopic != "browser:purge-session-history")
+ return;
+
+ let back = document.getElementById("cmd_back");
+ back.setAttribute("disabled", "true");
+ let forward = document.getElementById("cmd_forward");
+ forward.setAttribute("disabled", "true");
+
+ let urlbar = document.getElementById("urlbar-edit");
+ if (urlbar) {
+ // Clear undo history of the URL bar
+ urlbar.editor.transactionManager.clear();
+ }
+ }
+};
+
+var ActivityObserver = {
+ _inBackground : false,
+ _notActive : false,
+ _isDisplayOff : false,
+ _timeoutID: 0,
+ observe: function ao_observe(aSubject, aTopic, aData) {
+ if (aTopic == "application-background") {
+ this._inBackground = true;
+ } else if (aTopic == "application-foreground") {
+ this._inBackground = false;
+ } else if (aTopic == "system-idle") {
+ this._notActive = true;
+ } else if (aTopic == "system-active") {
+ this._notActive = false;
+ } else if (aTopic == "system-display-on") {
+ this._isDisplayOff = false;
+ } else if (aTopic == "system-display-off") {
+ this._isDisplayOff = true;
+ }
+ let activeTabState = !this._inBackground && !this._notActive && !this._isDisplayOff;
+ if (this._timeoutID)
+ clearTimeout(this._timeoutID);
+ if (Browser.selectedTab.active != activeTabState) {
+ // On Maemo all backgrounded applications getting portrait orientation
+ // so if browser had landscape mode then we need timeout in order
+ // to finish last rotate/paint operation and have nice lookine browser in TS
+ this._timeoutID = setTimeout(function() { Browser.selectedTab.active = activeTabState; }, activeTabState ? 0 : kSetInactiveStateTimeout);
+ }
+ }
+};
+
+function getNotificationBox(aBrowser) {
+ return Browser.getNotificationBox(aBrowser);
+}
+
+function showDownloadManager(aWindowContext, aID, aReason) {
+ PanelUI.show("downloads-container");
+ // TODO: select the download with aID
+}
+
+function Tab(aURI, aParams) {
+ this._id = null;
+ this._browser = null;
+ this._notification = null;
+ this._loading = false;
+ this._chromeTab = null;
+ this._metadata = null;
+
+ this.owner = null;
+
+ this.hostChanged = false;
+ this.state = null;
+
+ // Set to 0 since new tabs that have not been viewed yet are good tabs to
+ // toss if app needs more memory.
+ this.lastSelected = 0;
+
+ // aParams is an object that contains some properties for the initial tab
+ // loading like flags, a referrerURI, a charset or even a postData.
+ this.create(aURI, aParams || {});
+
+ // default tabs to inactive (i.e. no display port)
+ this.active = false;
+}
+
+Tab.prototype = {
+ get browser() {
+ return this._browser;
+ },
+
+ get notification() {
+ return this._notification;
+ },
+
+ get chromeTab() {
+ return this._chromeTab;
+ },
+
+ get metadata() {
+ return this._metadata || kDefaultMetadata;
+ },
+
+ /** Update browser styles when the viewport metadata changes. */
+ updateViewportMetadata: function updateViewportMetadata(aMetadata) {
+ if (aMetadata && aMetadata.autoScale) {
+ let scaleRatio = aMetadata.scaleRatio = Browser.getScaleRatio();
+
+ if ("defaultZoom" in aMetadata && aMetadata.defaultZoom > 0)
+ aMetadata.defaultZoom *= scaleRatio;
+ if ("minZoom" in aMetadata && aMetadata.minZoom > 0)
+ aMetadata.minZoom *= scaleRatio;
+ if ("maxZoom" in aMetadata && aMetadata.maxZoom > 0)
+ aMetadata.maxZoom *= scaleRatio;
+ }
+ this._metadata = aMetadata;
+ this.updateViewportSize();
+ },
+
+ /**
+ * Update browser size when the metadata or the window size changes.
+ */
+ updateViewportSize: function updateViewportSize(width, height) {
+ /* XXX Viewport resizing disabled because of bug 766142
+
+ let browser = this._browser;
+ if (!browser)
+ return;
+
+ let screenW = width || ContentAreaObserver.width;
+ let screenH = height || ContentAreaObserver.height;
+ let viewportW, viewportH;
+
+ let metadata = this.metadata;
+ if (metadata.autoSize) {
+ if ("scaleRatio" in metadata) {
+ viewportW = screenW / metadata.scaleRatio;
+ viewportH = screenH / metadata.scaleRatio;
+ } else {
+ viewportW = screenW;
+ viewportH = screenH;
+ }
+ } else {
+ viewportW = metadata.width;
+ viewportH = metadata.height;
+
+ // If (scale * width) < device-width, increase the width (bug 561413).
+ let maxInitialZoom = metadata.defaultZoom || metadata.maxZoom;
+ if (maxInitialZoom && viewportW)
+ viewportW = Math.max(viewportW, screenW / maxInitialZoom);
+
+ let validW = viewportW > 0;
+ let validH = viewportH > 0;
+
+ if (!validW)
+ viewportW = validH ? (viewportH * (screenW / screenH)) : Browser.defaultBrowserWidth;
+ if (!validH)
+ viewportH = viewportW * (screenH / screenW);
+ }
+
+ // Make sure the viewport height is not shorter than the window when
+ // the page is zoomed out to show its full width.
+ let pageZoomLevel = this.getPageZoomLevel(screenW);
+ let minScale = this.clampZoomLevel(pageZoomLevel, pageZoomLevel);
+ viewportH = Math.max(viewportH, screenH / minScale);
+
+ if (browser.contentWindowWidth != viewportW || browser.contentWindowHeight != viewportH)
+ browser.setWindowSize(viewportW, viewportH);
+ */
+ },
+
+ restoreViewportPosition: function restoreViewportPosition(aOldWidth, aNewWidth) {
+ let browser = this._browser;
+
+ // zoom to keep the same portion of the document visible
+ let oldScale = browser.scale;
+ let newScale = this.clampZoomLevel(oldScale * aNewWidth / aOldWidth);
+ let scaleRatio = newScale / oldScale;
+
+ let view = browser.getRootView();
+ let pos = view.getPosition();
+ browser.fuzzyZoom(newScale, pos.x * scaleRatio, pos.y * scaleRatio);
+ browser.finishFuzzyZoom();
+ },
+
+ startLoading: function startLoading() {
+ if (this._loading) throw "Already Loading!";
+ this._loading = true;
+ },
+
+ endLoading: function endLoading() {
+ if (!this._loading) throw "Not Loading!";
+ this._loading = false;
+ this.updateFavicon();
+ },
+
+ isLoading: function isLoading() {
+ return this._loading;
+ },
+
+ create: function create(aURI, aParams) {
+ this._chromeTab = Elements.tabList.addTab();
+ this._id = Browser.createTabId();
+ let browser = this._createBrowser(aURI, null);
+
+ // Should we fully load the new browser, or wait until later
+ if ("delayLoad" in aParams && aParams.delayLoad)
+ return;
+
+ try {
+ let flags = aParams.flags || Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
+ let postData = ("postData" in aParams && aParams.postData) ? aParams.postData.value : null;
+ let referrerURI = "referrerURI" in aParams ? aParams.referrerURI : null;
+ let charset = "charset" in aParams ? aParams.charset : null;
+ browser.loadURIWithFlags(aURI, flags, referrerURI, charset, postData);
+ } catch(e) {
+ dump("Error: " + e + "\n");
+ }
+ },
+
+ destroy: function destroy() {
+ Elements.tabList.removeTab(this._chromeTab);
+ this._chromeTab = null;
+ this._destroyBrowser();
+ },
+
+ resurrect: function resurrect() {
+ let dead = this._browser;
+ let active = this.active;
+
+ // Hold onto the session store data
+ let session = { data: dead.__SS_data, extra: dead.__SS_extdata };
+
+ // We need this data to correctly create and position the new browser
+ // If this browser is already a zombie, fallback to the session data
+ let currentURL = dead.__SS_restore ? session.data.entries[0].url : dead.currentURI.spec;
+ let sibling = dead.nextSibling;
+
+ // Destory and re-create the browser
+ this._destroyBrowser();
+ let browser = this._createBrowser(currentURL, sibling);
+ if (active)
+ this.active = true;
+
+ // Reattach session store data and flag this browser so it is restored on select
+ browser.__SS_data = session.data;
+ browser.__SS_extdata = session.extra;
+ browser.__SS_restore = true;
+ },
+
+ _createBrowser: function _createBrowser(aURI, aInsertBefore) {
+ if (this._browser)
+ throw "Browser already exists";
+
+ // Create a notification box around the browser. Note this includes
+ // the input overlay we use to shade content from input events when
+ // we're intercepting touch input.
+ let notification = this._notification = document.createElement("notificationbox");
+
+ let browser = this._browser = document.createElement("browser");
+ browser.id = "browser-" + this._id;
+ browser.setAttribute("class", "viewable-width viewable-height");
+ this._chromeTab.linkedBrowser = browser;
+
+ browser.setAttribute("type", "content");
+
+ let useRemote = Services.prefs.getBoolPref("browser.tabs.remote");
+ let useLocal = Util.isLocalScheme(aURI);
+ browser.setAttribute("remote", (!useLocal && useRemote) ? "true" : "false");
+
+ // Append the browser to the document, which should start the page load
+ notification.appendChild(browser);
+ Elements.browsers.insertBefore(notification, aInsertBefore);
+
+ // stop about:blank from loading
+ browser.stop();
+
+ let fl = browser.QueryInterface(Ci.nsIFrameLoaderOwner).frameLoader;
+ fl.renderMode = Ci.nsIFrameLoader.RENDER_MODE_ASYNC_SCROLL;
+
+ return browser;
+ },
+
+ _destroyBrowser: function _destroyBrowser() {
+ if (this._browser) {
+ let notification = this._notification;
+ let browser = this._browser;
+ browser.active = false;
+
+ this._notification = null;
+ this._browser = null;
+ this._loading = false;
+
+ Elements.browsers.removeChild(notification);
+ }
+ },
+
+ /**
+ * Takes a scale and restricts it based on this tab's zoom limits.
+ * @param aScale The original scale.
+ * @param aPageZoomLevel (optional) The zoom-to-fit scale, if known.
+ * This is a performance optimization to avoid extra calls.
+ */
+ clampZoomLevel: function clampZoomLevel(aScale, aPageZoomLevel) {
+ let md = this.metadata;
+ if (!this.allowZoom) {
+ return (md && md.defaultZoom)
+ ? md.defaultZoom
+ : (aPageZoomLevel || this.getPageZoomLevel());
+ }
+
+ let browser = this._browser;
+ let bounded = Util.clamp(aScale, ZoomManager.MIN, ZoomManager.MAX);
+
+ if (md && md.minZoom)
+ bounded = Math.max(bounded, md.minZoom);
+ if (md && md.maxZoom)
+ bounded = Math.min(bounded, md.maxZoom);
+
+ bounded = Math.max(bounded, this.getPageZoomLevel());
+
+ let rounded = Math.round(bounded * kBrowserViewZoomLevelPrecision) / kBrowserViewZoomLevelPrecision;
+ return rounded || 1.0;
+ },
+
+ /** Record the initial zoom level when a page first loads. */
+ resetZoomLevel: function resetZoomLevel() {
+ this._defaultZoomLevel = this._browser.scale;
+ },
+
+ scrolledAreaChanged: function scrolledAreaChanged(firstPaint) {
+ if (!this._browser)
+ return;
+
+ if (firstPaint) {
+ // You only get one shot, do not miss your chance to reflow.
+ this.updateViewportSize();
+ }
+
+ this.updateDefaultZoomLevel();
+ },
+
+ /**
+ * Recalculate default zoom level when page size changes, and update zoom
+ * level if we are at default.
+ */
+ updateDefaultZoomLevel: function updateDefaultZoomLevel() {
+ let browser = this._browser;
+ if (!browser || !this._firstPaint)
+ return;
+
+ let isDefault = this.isDefaultZoomLevel();
+ this._defaultZoomLevel = this.getDefaultZoomLevel();
+ if (isDefault) {
+ if (browser.scale != this._defaultZoomLevel) {
+ browser.scale = this._defaultZoomLevel;
+ } else {
+ // If the scale level has not changed we want to be sure the content
+ // render correctly since the page refresh process could have been
+ // stalled during page load. In this case if the page has the exact
+ // same width (like the same page, so by doing 'refresh') and the
+ // page was scrolled the content is just checkerboard at this point
+ // and this call ensure we render it correctly.
+ browser.getRootView()._updateCacheViewport();
+ }
+ } else {
+ // if we are reloading, the page will retain its scale. if it is zoomed
+ // we need to refresh the viewport so that we do not show checkerboard
+ browser.getRootView()._updateCacheViewport();
+ }
+ },
+
+ isDefaultZoomLevel: function isDefaultZoomLevel() {
+ return this._browser.scale == this._defaultZoomLevel;
+ },
+
+ getDefaultZoomLevel: function getDefaultZoomLevel() {
+ let md = this.metadata;
+ if (md && md.defaultZoom)
+ return this.clampZoomLevel(md.defaultZoom);
+
+ let browserWidth = this._browser.getBoundingClientRect().width;
+ let defaultZoom = browserWidth / this._browser.contentWindowWidth;
+ return this.clampZoomLevel(defaultZoom);
+ },
+
+ /**
+ * @param aScreenWidth (optional) The width of the browser widget, if known.
+ * This is a performance optimization to save extra calls to getBoundingClientRect.
+ * @return The scale at which the browser will be zoomed out to fit the document width.
+ */
+ getPageZoomLevel: function getPageZoomLevel(aScreenWidth) {
+ let browserW = this._browser.contentDocumentWidth;
+ if (browserW == 0)
+ return 1.0;
+
+ let screenW = aScreenWidth || this._browser.getBoundingClientRect().width;
+ return screenW / browserW;
+ },
+
+ get allowZoom() {
+ return this.metadata.allowZoom && !Util.isURLEmpty(this.browser.currentURI.spec);
+ },
+
+ updateThumbnailSource: function updateThumbnailSource() {
+ this._chromeTab.updateThumbnailSource(this._browser);
+ },
+
+ updateFavicon: function updateFavicon() {
+ this._chromeTab.updateFavicon(this._browser.mIconURL);
+ },
+
+ set active(aActive) {
+ if (!this._browser)
+ return;
+
+ let notification = this._notification;
+ let browser = this._browser;
+
+ if (aActive) {
+ browser.setAttribute("type", "content-primary");
+ Elements.browsers.selectedPanel = notification;
+ browser.active = true;
+ Elements.tabList.selectedTab = this._chromeTab;
+ browser.focus();
+ } else {
+ browser.messageManager.sendAsyncMessage("Browser:Blur", { });
+ browser.setAttribute("type", "content");
+ browser.active = false;
+ }
+ },
+
+ get active() {
+ if (!this._browser)
+ return false;
+ return this._browser.getAttribute("type") == "content-primary";
+ },
+
+ toString: function() {
+ return "[Tab " + (this._browser ? this._browser.currentURI.spec : "(no browser)") + "]";
+ }
+};
+
+// Helper used to hide IPC / non-IPC differences for rendering to a canvas
+function rendererFactory(aBrowser, aCanvas) {
+ let wrapper = {};
+
+ if (aBrowser.contentWindow) {
+ let ctx = aCanvas.getContext("2d");
+ let draw = function(browser, aLeft, aTop, aWidth, aHeight, aColor, aFlags) {
+ ctx.drawWindow(browser.contentWindow, aLeft, aTop, aWidth, aHeight, aColor, aFlags);
+ let e = document.createEvent("HTMLEvents");
+ e.initEvent("MozAsyncCanvasRender", true, true);
+ aCanvas.dispatchEvent(e);
+ };
+ wrapper.checkBrowser = function(browser) {
+ return browser.contentWindow;
+ };
+ wrapper.drawContent = function(callback) {
+ callback(ctx, draw);
+ };
+ }
+ else {
+ let ctx = aCanvas.MozGetIPCContext("2d");
+ let draw = function(browser, aLeft, aTop, aWidth, aHeight, aColor, aFlags) {
+ ctx.asyncDrawXULElement(browser, aLeft, aTop, aWidth, aHeight, aColor, aFlags);
+ };
+ wrapper.checkBrowser = function(browser) {
+ return !browser.contentWindow;
+ };
+ wrapper.drawContent = function(callback) {
+ callback(ctx, draw);
+ };
+ }
+
+ return wrapper;
+};
+
+var ContentAreaObserver = {
+ styles: {},
+
+ // In desktop mode avoids breaking window dims before
+ // the desktop window is displayed.
+ get width() { return window.innerWidth || 1366; },
+ get height() { return window.innerHeight || 768; },
+
+ get contentHeight() {
+ return this._getContentHeightForWindow(this.height);
+ },
+
+ get contentTop () {
+ return Elements.toolbar.getBoundingClientRect().bottom;
+ },
+
+ get viewableHeight() {
+ return this._getViewableHeightForContent(this.contentHeight);
+ },
+
+ get isKeyboardOpened() { return MetroUtils.keyboardVisible; },
+ get hasVirtualKeyboard() { return true; },
+
+ init: function cao_init() {
+ window.addEventListener("resize", this, false);
+
+ let os = Services.obs;
+ os.addObserver(this, "metro_softkeyboard_shown", false);
+ os.addObserver(this, "metro_softkeyboard_hidden", false);
+
+ // Create styles for the following class names. The 'width' and 'height'
+ // properties of these styles are updated whenever various parts of the UI
+ // are resized.
+ //
+ // * window-width, window-height: The innerWidth/innerHeight of the main
+ // chrome window.
+ // * content-width, content-height: The area of the window dedicated to web
+ // content; this is equal to the innerWidth/Height minus any toolbars
+ // or similar chrome.
+ // * viewable-width, viewable-height: The portion of the content area that
+ // is not obscured by the on-screen keyboard.
+ let stylesheet = document.styleSheets[0];
+ for (let style of ["window-width", "window-height",
+ "content-height", "content-width",
+ "viewable-height", "viewable-width"]) {
+ let index = stylesheet.insertRule("." + style + " {}", stylesheet.cssRules.length);
+ this.styles[style] = stylesheet.cssRules[index].style;
+ }
+ this.update();
+ },
+
+ uninit: function cao_uninit() {
+ let os = Services.obs;
+ os.removeObserver(this, "metro_softkeyboard_shown");
+ os.removeObserver(this, "metro_softkeyboard_hidden");
+ },
+
+ update: function cao_update (width, height) {
+ let oldWidth = parseInt(this.styles["window-width"].width);
+ let oldHeight = parseInt(this.styles["window-height"].height);
+
+ let newWidth = width || this.width;
+ let newHeight = height || this.height;
+
+ if (newHeight == oldHeight && newWidth == oldWidth)
+ return;
+
+ this.styles["window-width"].width = newWidth + "px";
+ this.styles["window-width"].maxWidth = newWidth + "px";
+ this.styles["window-height"].height = newHeight + "px";
+ this.styles["window-height"].maxHeight = newHeight + "px";
+
+ let isStartup = !oldHeight && !oldWidth;
+ for (let i = Browser.tabs.length - 1; i >=0; i--) {
+ let tab = Browser.tabs[i];
+ let oldContentWindowWidth = tab.browser.contentWindowWidth;
+ tab.updateViewportSize(newWidth, newHeight); // contentWindowWidth may change here.
+
+ // Don't bother updating the zoom level on startup
+ if (!isStartup) {
+ // If the viewport width is still the same, the page layout has not
+ // changed, so we can keep keep the same content on-screen.
+ if (tab.browser.contentWindowWidth == oldContentWindowWidth)
+ tab.restoreViewportPosition(oldWidth, newWidth);
+
+ tab.updateDefaultZoomLevel();
+ }
+ }
+
+ // We want to keep the current focused element into view if possible
+ if (!isStartup) {
+ let currentElement = document.activeElement;
+ let [scrollbox, scrollInterface] = ScrollUtils.getScrollboxFromElement(currentElement);
+ if (scrollbox && scrollInterface && currentElement && currentElement != scrollbox) {
+ // retrieve the direct child of the scrollbox
+ while (currentElement.parentNode != scrollbox)
+ currentElement = currentElement.parentNode;
+
+ setTimeout(function() { scrollInterface.ensureElementIsVisible(currentElement) }, 0);
+ }
+ }
+
+ this.updateContentArea(newWidth, this._getContentHeightForWindow(newHeight));
+ this._fire("SizeChanged");
+ },
+
+ updateContentArea: function cao_updateContentArea (width, height) {
+ let oldHeight = parseInt(this.styles["content-height"].height);
+ let oldWidth = parseInt(this.styles["content-width"].width);
+
+ let newWidth = width || this.width;
+ let newHeight = height || this.contentHeight;
+
+ if (newHeight == oldHeight && newWidth == oldWidth)
+ return;
+
+ this.styles["content-height"].height = newHeight + "px";
+ this.styles["content-height"].maxHeight = newHeight + "px";
+ this.styles["content-width"].width = newWidth + "px";
+ this.styles["content-width"].maxWidth = newWidth + "px";
+
+ this.updateViewableArea(newWidth, this._getViewableHeightForContent(newHeight));
+ this._fire("ContentSizeChanged");
+ },
+
+ updateViewableArea: function cao_updateViewableArea (width, height) {
+ let oldHeight = parseInt(this.styles["viewable-height"].height);
+ let oldWidth = parseInt(this.styles["viewable-width"].width);
+
+ let newWidth = width || this.width;
+ let newHeight = height || this.viewableHeight;
+
+ if (newHeight == oldHeight && newWidth == oldWidth)
+ return;
+
+ this.styles["viewable-height"].height = newHeight + "px";
+ this.styles["viewable-height"].maxHeight = newHeight + "px";
+ this.styles["viewable-width"].width = newWidth + "px";
+ this.styles["viewable-width"].maxWidth = newWidth + "px";
+
+ this._fire("ViewableSizeChanged");
+ },
+
+ observe: function cao_observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "metro_softkeyboard_shown":
+ case "metro_softkeyboard_hidden": {
+ let event = document.createEvent("UIEvents");
+ let eventDetails = {
+ opened: aTopic == "metro_softkeyboard_shown",
+ details: aData
+ };
+
+ event.initUIEvent("KeyboardChanged", true, false, window, eventDetails);
+ window.dispatchEvent(event);
+
+ this.updateViewableArea();
+ break;
+ }
+ };
+ },
+
+ handleEvent: function cao_handleEvent(anEvent) {
+ switch (anEvent.type) {
+ case 'resize':
+ if (anEvent.target != window)
+ return;
+
+ ContentAreaObserver.update();
+ break;
+ }
+ },
+
+ _fire: function (aName) {
+ // setTimeout(callback, 0) to ensure the resize event handler dispatch is finished
+ setTimeout(function() {
+ let event = document.createEvent("Events");
+ event.initEvent(aName, true, false);
+ Elements.browsers.dispatchEvent(event);
+ }, 0);
+ },
+
+ _getContentHeightForWindow: function (windowHeight) {
+ let contextUIHeight = BrowserUI.isTabsOnly ? Elements.toolbar.getBoundingClientRect().bottom : 0;
+ return windowHeight - contextUIHeight;
+ },
+
+ _getViewableHeightForContent: function (contentHeight) {
+ let keyboardHeight = MetroUtils.keyboardHeight;
+ return contentHeight - keyboardHeight;
+ }
+};
diff --git a/browser/metro/base/content/browser.xul b/browser/metro/base/content/browser.xul
new file mode 100644
index 000000000000..345b70b4634c
--- /dev/null
+++ b/browser/metro/base/content/browser.xul
@@ -0,0 +1,691 @@
+
+
+
+
+
+
+
+
+
+
+%globalDTD;
+
+%browserDTD;
+
+%brandDTD;
+
+%prefsDTD;
+#ifdef MOZ_SERVICES_SYNC
+
+%syncDTD;
+#endif
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#ifdef MOZ_SERVICES_SYNC
+
+#endif
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#expand
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#ifdef MOZ_SERVICES_SYNC
+
+
+
+
+
+
+
+#endif
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/metro/base/content/commandUtil.js b/browser/metro/base/content/commandUtil.js
new file mode 100644
index 000000000000..41b107b1b08c
--- /dev/null
+++ b/browser/metro/base/content/commandUtil.js
@@ -0,0 +1,164 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+/**
+ * Command Updater
+ */
+var CommandUpdater = {
+ /**
+ * Gets a controller that can handle a particular command.
+ * @param command
+ * A command to locate a controller for, preferring controllers that
+ * show the command as enabled.
+ * @returns In this order of precedence:
+ * - the first controller supporting the specified command
+ * associated with the focused element that advertises the
+ * command as ENABLED
+ * - the first controller supporting the specified command
+ * associated with the global window that advertises the
+ * command as ENABLED
+ * - the first controller supporting the specified command
+ * associated with the focused element
+ * - the first controller supporting the specified command
+ * associated with the global window
+ */
+ _getControllerForCommand: function(command) {
+ try {
+ var controller = top.document.commandDispatcher.getControllerForCommand(command);
+ if (controller && controller.isCommandEnabled(command))
+ return controller;
+ }
+ catch(e) {
+ }
+ var controllerCount = window.controllers.getControllerCount();
+ for (var i = 0; i < controllerCount; ++i) {
+ var current = window.controllers.getControllerAt(i);
+ try {
+ if (current.supportsCommand(command) && current.isCommandEnabled(command))
+ return current;
+ }
+ catch (e) {
+ }
+ }
+ return controller || window.controllers.getControllerForCommand(command);
+ },
+
+ /**
+ * Updates the state of a XUL element for the specified command
+ * depending on its state.
+ * @param command
+ * The name of the command to update the XUL element for
+ */
+ updateCommand: function(command) {
+ var enabled = false;
+ try {
+ var controller = this._getControllerForCommand(command);
+ if (controller) {
+ enabled = controller.isCommandEnabled(command);
+ }
+ }
+ catch(ex) { }
+
+ this.enableCommand(command, enabled);
+ },
+
+ /**
+ * Updates the state of a XUL element for the specified command
+ * depending on its state.
+ * @param command
+ * The name of the command to update the XUL element for
+ */
+ updateCommands: function(_commands) {
+ var commands = _commands.split(",");
+ for (var command in commands) {
+ this.updateCommand(commands[command]);
+ }
+ },
+
+ /**
+ * Enables or disables a XUL element.
+ * @param command
+ * The name of the command to enable or disable
+ * @param enabled
+ * true if the command should be enabled, false otherwise.
+ */
+ enableCommand: function(command, enabled) {
+ var element = document.getElementById(command);
+ if (!element)
+ return;
+ if (enabled)
+ element.removeAttribute("disabled");
+ else
+ element.setAttribute("disabled", "true");
+ },
+
+ /**
+ * Performs the action associated with a specified command using the most
+ * relevant controller.
+ * @param command
+ * The command to perform.
+ */
+ doCommand: function(command) {
+ var controller = this._getControllerForCommand(command);
+ if (!controller)
+ return;
+ controller.doCommand(command);
+ },
+
+ /**
+ * Changes the label attribute for the specified command.
+ * @param command
+ * The command to update.
+ * @param labelAttribute
+ * The label value to use.
+ */
+ setMenuValue: function(command, labelAttribute) {
+ var commandNode = top.document.getElementById(command);
+ if (commandNode)
+ {
+ var label = commandNode.getAttribute(labelAttribute);
+ if ( label )
+ commandNode.setAttribute('label', label);
+ }
+ },
+
+ /**
+ * Changes the accesskey attribute for the specified command.
+ * @param command
+ * The command to update.
+ * @param valueAttribute
+ * The value attribute to use.
+ */
+ setAccessKey: function(command, valueAttribute) {
+ var commandNode = top.document.getElementById(command);
+ if (commandNode)
+ {
+ var value = commandNode.getAttribute(valueAttribute);
+ if ( value )
+ commandNode.setAttribute('accesskey', value);
+ }
+ },
+
+ /**
+ * Inform all the controllers attached to a node that an event has occurred
+ * (e.g. the tree controllers need to be informed of blur events so that they can change some of the
+ * menu items back to their default values)
+ * @param node
+ * The node receiving the event
+ * @param event
+ * The event.
+ */
+ onEvent: function(node, event) {
+ var numControllers = node.controllers.getControllerCount();
+ var controller;
+
+ for ( var controllerIndex = 0; controllerIndex < numControllers; controllerIndex++ )
+ {
+ controller = node.controllers.getControllerAt(controllerIndex);
+ if ( controller )
+ controller.onEvent(event);
+ }
+ }
+};
diff --git a/browser/metro/base/content/config.js b/browser/metro/base/content/config.js
new file mode 100644
index 000000000000..f5cbcf6200fb
--- /dev/null
+++ b/browser/metro/base/content/config.js
@@ -0,0 +1,391 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+let Ci = Components.interfaces;
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+var ViewConfig = {
+ get _container() {
+ delete this._container;
+ return this._container = document.getElementById("prefs-container");
+ },
+
+ get _editor() {
+ delete this._editor;
+ return this._editor = document.getElementById("editor");
+ },
+
+ init: function init() {
+ window.addEventListener("resize", this, false);
+ window.addEventListener("prefchange", this, false);
+ window.addEventListener("prefnew", this, false);
+
+ this._handleWindowResize();
+ this.filter("");
+
+ document.getElementById("textbox").focus();
+ },
+
+ uninit: function uninit() {
+ window.removeEventListener("resize", this, false);
+ window.removeEventListener("prefchange", this, false);
+ window.removeEventListener("prefnew", this, false);
+ },
+
+ filter: function filter(aValue) {
+ let row = document.getElementById("editor-row");
+ row.setAttribute("hidden", aValue != "");
+
+ let container = this._container;
+ container.scrollBoxObject.scrollTo(0, 0);
+ // Clear the list by replacing with a shallow copy
+ let empty = container.cloneNode(false);
+ empty.appendChild(row);
+ container.parentNode.replaceChild(empty, container);
+ this._container = empty;
+
+ let result = Utils.getPrefs(aValue);
+ this._container.setItems(result.map(this._createItem, this));
+ },
+
+ open: function open(aType) {
+ let buttons = document.getElementById("editor-buttons-add");
+ buttons.setAttribute("hidden", "true");
+
+ let shouldFocus = false;
+ let setting = document.getElementById("editor-setting");
+ switch (aType) {
+ case Ci.nsIPrefBranch.PREF_INT:
+ setting.setAttribute("type", "integer");
+ setting.setAttribute("min", -Infinity);
+ break;
+ case Ci.nsIPrefBranch.PREF_BOOL:
+ setting.setAttribute("type", "bool");
+ break;
+ case Ci.nsIPrefBranch.PREF_STRING:
+ setting.setAttribute("type", "string");
+ break;
+ }
+
+ setting.removeAttribute("title");
+ setting.removeAttribute("pref");
+ if (setting.input)
+ setting.input.value = "";
+
+ document.getElementById("editor-container").appendChild(this._editor);
+ let nameField = document.getElementById("editor-name");
+ nameField.value = "";
+
+ this._editor.setAttribute("hidden", "false");
+ this._currentItem = null;
+ nameField.focus();
+ },
+
+ close: function close(aValid) {
+ this._editor.setAttribute("hidden", "true");
+ let buttons = document.getElementById("editor-buttons-add");
+ buttons.setAttribute("hidden", "false");
+
+ if (aValid) {
+ let name = document.getElementById("editor-name").inputField.value;
+ if (name != "") {
+ let setting = document.getElementById("editor-setting");
+ setting.setAttribute("pref", name);
+ setting.valueToPreference();
+ }
+ }
+
+ document.getElementById("editor-container").appendChild(this._editor);
+ },
+
+ _currentItem: null,
+
+ delayEdit: function(aItem) {
+ setTimeout(this.edit.bind(this), 0, aItem);
+ },
+
+ edit: function(aItem) {
+ if (!aItem)
+ return;
+
+ let pref = Utils.getPref(aItem.getAttribute("name"));
+ if (pref.lock || !pref.name || aItem == this._currentItem)
+ return;
+
+ this.close(false);
+ this._currentItem = aItem;
+
+ let setting = document.getElementById("editor-setting");
+ let shouldFocus = false;
+ switch (pref.type) {
+ case Ci.nsIPrefBranch.PREF_BOOL:
+ setting.setAttribute("type", "bool");
+ break;
+
+ case Ci.nsIPrefBranch.PREF_INT:
+ setting.setAttribute("type", "integer");
+ setting.setAttribute("increment", this.getIncrementForValue(pref.value));
+ setting.setAttribute("min", -Infinity);
+ shouldFocus = true;
+ break;
+
+ case Ci.nsIPrefBranch.PREF_STRING:
+ setting.setAttribute("type", "string");
+ shouldFocus = true;
+ break;
+ }
+
+ setting.setAttribute("title", pref.name);
+ setting.setAttribute("pref", pref.name);
+
+ this._container.insertBefore(this._editor, aItem);
+
+ let resetButton = document.getElementById("editor-reset");
+ resetButton.setAttribute("disabled", pref.default);
+
+ this._editor.setAttribute("default", pref.default);
+ this._editor.setAttribute("hidden", "false");
+
+ if (shouldFocus && setting.input)
+ setting.input.focus();
+ },
+
+ reset: function reset(aItem) {
+ let setting = document.getElementById("editor-setting");
+ let pref = Utils.getPref(setting.getAttribute("pref"));
+ if (!pref.default)
+ Utils.resetPref(pref.name);
+ },
+
+ handleEvent: function handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "resize":
+ this._handleWindowResize();
+ break;
+
+ case "prefchange":
+ case "prefnew":
+ this._handlePrefChange(aEvent.detail, aEvent.type == "prefnew");
+ break;
+ }
+ },
+
+ _handleWindowResize: function _handleWindowResize() {
+ let mainBox = document.getElementById("main-container");
+ let textbox = document.getElementById("textbox");
+ let height = window.innerHeight - textbox.getBoundingClientRect().height;
+
+ mainBox.setAttribute("height", height);
+ },
+
+ _handlePrefChange: function _handlePrefChange(aIndex, aNew) {
+ let isEditing = !this._editor.hidden;
+ let shouldUpdateEditor = false;
+ if (isEditing) {
+ let setting = document.getElementById("editor-setting");
+ let editorIndex = Utils.getPrefIndex(setting.getAttribute("pref"));
+ shouldUpdateEditor = (aIndex == editorIndex);
+ if(shouldUpdateEditor || aIndex > editorIndex)
+ aIndex += 1;
+ }
+
+ // XXX An item display value will probably fail if a pref is changed in the
+ // background while there is a filter on the pref
+ let item = shouldUpdateEditor ? this._editor.nextSibling
+ : this._container.childNodes[aIndex + 1];// add 1 because of the new pref row
+ if (!item) // the pref is not viewable
+ return;
+
+ if (aNew) {
+ let pref = Utils.getPrefByIndex(aIndex);
+ let row = this._createItem(pref);
+ this._container.insertBefore(row, item);
+ return;
+ }
+
+ let pref = Utils.getPref(item.getAttribute("name"));
+ if (shouldUpdateEditor) {
+ this._editor.setAttribute("default", pref.default);
+
+ let resetButton = document.getElementById("editor-reset");
+ resetButton.disabled = pref.default;
+ }
+
+ item.setAttribute("default", pref.default);
+ item.lastChild.setAttribute("value", pref.value);
+ },
+
+ _createItem: function _createItem(aPref) {
+ let row = document.createElement("richlistitem");
+
+ row.setAttribute("name", aPref.name);
+ row.setAttribute("type", aPref.type);
+ row.setAttribute("role", "button");
+ row.setAttribute("default", aPref.default);
+
+ let label = document.createElement("label");
+ label.setAttribute("class", "preferences-title");
+ label.setAttribute("value", aPref.name);
+ label.setAttribute("crop", "end");
+ row.appendChild(label);
+
+ label = document.createElement("label");
+ label.setAttribute("class", "preferences-value");
+ label.setAttribute("value", aPref.value);
+ label.setAttribute("crop", "end");
+ row.appendChild(label);
+
+ return row;
+ },
+
+ getIncrementForValue: function getIncrementForValue(aValue) {
+ let count = 1;
+ while (aValue >= 100) {
+ aValue /= 10;
+ count *= 10;
+ }
+ return count;
+ }
+};
+
+var Utils = {
+ QueryInterface: function(aIID) {
+ if (!aIID.equals(Ci.nsIObserver) && !aIID.equals(Ci.nsISupportsWeakReference))
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ return this;
+ },
+
+ get _branch() {
+ delete this._branch;
+ this._branch = Services.prefs.getBranch(null);
+ this._branch.addObserver("", this, true);
+ return this._branch;
+ },
+
+ get _preferences() {
+ delete this._preferences;
+ let list = this._branch.getChildList("", {}).filter(function(element) {
+ return !(/^capability\./.test(element));
+ });
+ return this._preferences = list.sort().map(this.getPref, this);
+ },
+
+ getPrefs: function getPrefs(aValue) {
+ let result = this._preferences.slice();;
+ if (aValue != "") {
+ let reg = this._generateRegexp(aValue);
+ if (!reg)
+ return [];
+
+ result = this._preferences.filter(function(element, index, array) {
+ return reg.test(element.name + ";" + element.value);
+ });
+ }
+
+ return result;
+ },
+
+ getPref: function getPref(aPrefName) {
+ let branch = this._branch;
+ let pref = {
+ name: aPrefName,
+ value: "",
+ default: !branch.prefHasUserValue(aPrefName),
+ lock: branch.prefIsLocked(aPrefName),
+ type: branch.getPrefType(aPrefName)
+ };
+
+ try {
+ switch (pref.type) {
+ case Ci.nsIPrefBranch.PREF_BOOL:
+ pref.value = branch.getBoolPref(aPrefName).toString();
+ break;
+ case Ci.nsIPrefBranch.PREF_INT:
+ pref.value = branch.getIntPref(aPrefName).toString();
+ break;
+ default:
+ case Ci.nsIPrefBranch.PREF_STRING:
+ pref.value = branch.getComplexValue(aPrefName, Ci.nsISupportsString).data;
+ // Try in case it's a localized string (will throw an exception if not)
+ if (pref.default && /^chrome:\/\/.+\/locale\/.+\.properties/.test(pref.value))
+ pref.value = branch.getComplexValue(aPrefName, Ci.nsIPrefLocalizedString).data;
+ break;
+ }
+ } catch (e) {}
+
+ return pref;
+ },
+
+ getPrefByIndex: function getPrefByIndex(aIndex) {
+ return this._preferences[aIndex];
+ },
+
+ getPrefIndex: function getPrefIndex(aPrefName) {
+ let prefs = this._preferences;
+ let high = prefs.length - 1;
+ let low = 0, middle, element;
+
+ while (low <= high) {
+ middle = parseInt((low + high) / 2);
+ element = prefs[middle];
+
+ if (element.name > aPrefName)
+ high = middle - 1;
+ else if (element.name < aPrefName)
+ low = middle + 1;
+ else
+ return middle;
+ }
+
+ return -1;
+ },
+
+ resetPref: function resetPref(aPrefName) {
+ this._branch.clearUserPref(aPrefName);
+ },
+
+ observe: function observe(aSubject, aTopic, aPrefName) {
+ if (aTopic != "nsPref:changed" || /^capability\./.test(aPrefName)) // avoid displaying "private" preferences
+ return;
+
+ let type = "prefchange";
+ let index = this.getPrefIndex(aPrefName);
+ if (index != - 1) {
+ // update the inner array
+ let pref = this.getPref(aPrefName);
+ this._preferences[index].value = pref.value;
+ }
+ else {
+ // XXX we could do better here
+ let list = this._branch.getChildList("", {}).filter(function(element, index, array) {
+ return !(/^capability\./.test(element));
+ });
+ this._preferences = list.sort().map(this.getPref, this);
+
+ type = "prefnew";
+ index = this.getPrefIndex(aPrefName);
+ }
+
+ let evt = document.createEvent("UIEvents");
+ evt.initUIEvent(type, true, true, window, index);
+ window.dispatchEvent(evt);
+ },
+
+ _generateRegexp: function _generateRegexp(aValue) {
+ if (aValue.charAt(0) == "/") {
+ try {
+ let rv = aValue.match(/^\/(.*)\/(i?)$/);
+ return RegExp(rv[1], rv[2]);
+ }
+ catch (e) {
+ return null; // Do nothing on incomplete or bad RegExp
+ }
+ }
+
+ return RegExp(aValue.replace(/([^* \w])/g, "\\$1").replace(/^\*+/, "")
+ .replace(/\*+/g, ".*"), "i");
+ }
+};
+
diff --git a/browser/metro/base/content/config.xul b/browser/metro/base/content/config.xul
new file mode 100644
index 000000000000..629f5970ecd2
--- /dev/null
+++ b/browser/metro/base/content/config.xul
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+%configDTD;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/metro/base/content/console.js b/browser/metro/base/content/console.js
new file mode 100644
index 000000000000..3119682b11b6
--- /dev/null
+++ b/browser/metro/base/content/console.js
@@ -0,0 +1,298 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+let ConsolePanelView = {
+ _list: null,
+ _inited: false,
+ _evalTextbox: null,
+ _evalFrame: null,
+ _evalCode: "",
+ _bundle: null,
+ _showChromeErrors: -1,
+ _enabledPref: "devtools.errorconsole.enabled",
+
+ init: function cv_init() {
+ if (this._list)
+ return;
+
+ this._list = document.getElementById("console-box");
+ this._evalTextbox = document.getElementById("console-eval-textbox");
+ this._bundle = Strings.browser;
+
+ this._count = 0;
+ this.limit = 250;
+
+ try {
+ // update users using the legacy pref
+ if (Services.prefs.getBoolPref("browser.console.showInPanel")) {
+ Services.prefs.setBoolPref(this._enabledPref, true);
+ Services.prefs.clearUserPref("browser.console.showInPanel");
+ }
+ } catch(ex) {
+ // likely don't have an old pref
+ }
+ this.updateVisibility();
+ Services.prefs.addObserver(this._enabledPref, this, false);
+ },
+
+ show: function show() {
+ if (this._inited)
+ return;
+ this._inited = true;
+
+ this.init(); // In case the panel is selected before init has been called.
+
+ Services.console.registerListener(this);
+
+ this.appendInitialItems();
+
+ // Delay creation of the iframe for startup performance
+ this._evalFrame = document.createElement("iframe");
+ this._evalFrame.id = "console-evaluator";
+ this._evalFrame.collapsed = true;
+ document.getElementById("console-container").appendChild(this._evalFrame);
+
+ this._evalFrame.addEventListener("load", this.loadOrDisplayResult.bind(this), true);
+ },
+
+ uninit: function cv_uninit() {
+ if (this._inited)
+ Services.console.unregisterListener(this);
+
+ Services.prefs.removeObserver(this._enabledPref, this, false);
+ },
+
+ get enabled() {
+ return Services.prefs.getBoolPref(this._enabledPref);
+ },
+
+ updateVisibility: function ec_updateVisibility(aVal, aPref) {
+ let button = document.getElementById("menuitem-console");
+ button.hidden = !this.enabled;
+ Appbar._updateDebugButtons();
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == "nsPref:changed")
+ this.updateVisibility();
+ else
+ this.appendItem(aSubject);
+ },
+
+ showChromeErrors: function() {
+ if (this._showChromeErrors != -1)
+ return this._showChromeErrors;
+
+ try {
+ let pref = Services.prefs;
+ return this._showChromeErrors = pref.getBoolPref("javascript.options.showInConsole");
+ }
+ catch(ex) {
+ return this._showChromeErrors = false;
+ }
+ },
+
+ appendItem: function cv_appendItem(aObject) {
+ try {
+ // Try to QI it to a script error to get more info
+ let scriptError = aObject.QueryInterface(Ci.nsIScriptError);
+
+ // filter chrome urls
+ if (!this.showChromeErrors && scriptError.sourceName.substr(0, 9) == "chrome://")
+ return;
+ this.appendError(scriptError);
+ }
+ catch (ex) {
+ try {
+ // Try to QI it to a console message
+ let msg = aObject.QueryInterface(Ci.nsIConsoleMessage);
+
+ if (msg.message)
+ this.appendMessage(msg.message);
+ else // observed a null/"clear" message
+ this.clearConsole();
+ }
+ catch (ex2) {
+ // Give up and append the object itself as a string
+ this.appendMessage(aObject);
+ }
+ }
+ },
+
+ appendError: function cv_appendError(aObject) {
+ let row = this.createConsoleRow();
+ let nsIScriptError = Ci.nsIScriptError;
+
+ // Is this error actually just a non-fatal warning?
+ let warning = aObject.flags & nsIScriptError.warningFlag != 0;
+
+ let typetext = warning ? "typeWarning" : "typeError";
+ row.setAttribute("typetext", this._bundle.GetStringFromName(typetext));
+ row.setAttribute("type", warning ? "warning" : "error");
+ row.setAttribute("msg", aObject.errorMessage);
+ row.setAttribute("category", aObject.category);
+ if (aObject.lineNumber || aObject.sourceName) {
+ row.setAttribute("href", aObject.sourceName);
+ row.setAttribute("line", aObject.lineNumber);
+ }
+ else {
+ row.setAttribute("hideSource", "true");
+ }
+ if (aObject.sourceLine) {
+ row.setAttribute("code", aObject.sourceLine.replace(/\s/g, " "));
+ if (aObject.columnNumber) {
+ row.setAttribute("col", aObject.columnNumber);
+ row.setAttribute("errorDots", this.repeatChar(" ", aObject.columnNumber));
+ row.setAttribute("errorCaret", " ");
+ }
+ else {
+ row.setAttribute("hideCaret", "true");
+ }
+ }
+ else {
+ row.setAttribute("hideCode", "true");
+ }
+
+ let mode = document.getElementById("console-filter").value;
+ if (mode != "all" && mode != row.getAttribute("type"))
+ row.collapsed = true;
+
+ this.appendConsoleRow(row);
+ },
+
+ appendMessage: function cv_appendMessage (aMessage) {
+ let row = this.createConsoleRow();
+ row.setAttribute("type", "message");
+ row.setAttribute("msg", aMessage);
+
+ let mode = document.getElementById("console-filter").value;
+ if (mode != "all" && mode != "message")
+ row.collapsed = true;
+
+ this.appendConsoleRow(row);
+ },
+
+ createConsoleRow: function cv_createConsoleRow() {
+ let row = document.createElement("richlistitem");
+ row.setAttribute("class", "console-row");
+ return row;
+ },
+
+ appendConsoleRow: function cv_appendConsoleRow(aRow) {
+ this._list.appendChild(aRow);
+ if (++this._count > this.limit)
+ this.deleteFirst();
+ },
+
+ deleteFirst: function cv_deleteFirst() {
+ let node = this._list.firstChild;
+ this._list.removeChild(node);
+ --this._count;
+ },
+
+ appendInitialItems: function cv_appendInitialItems() {
+ let messages = Services.console.getMessageArray();
+
+ // In case getMessageArray returns 0-length array as null
+ if (!messages)
+ messages = [];
+
+ let limit = messages.length - this.limit;
+ if (limit < 0)
+ limit = 0;
+
+ // Checks if console ever been cleared
+ for (var i = messages.length - 1; i >= limit; --i)
+ if (!messages[i].message)
+ break;
+
+ // Populate with messages after latest "clear"
+ while (++i < messages.length)
+ this.appendItem(messages[i]);
+ },
+
+ clearConsole: function cv_clearConsole() {
+ if (this._count == 0) // already clear
+ return;
+ this._count = 0;
+
+ let newRows = this._list.cloneNode(false);
+ this._list.parentNode.replaceChild(newRows, this._list);
+ this._list = newRows;
+ this.selectedItem = null;
+ },
+
+ changeMode: function cv_changeMode() {
+ let mode = document.getElementById("console-filter").value;
+ if (this._list.getAttribute("mode") != mode) {
+ let rows = this._list.childNodes;
+ for (let i=0; i < rows.length; i++) {
+ let row = rows[i];
+ if (mode == "all" || row.getAttribute ("type") == mode)
+ row.collapsed = false;
+ else
+ row.collapsed = true;
+ }
+ this._list.mode = mode;
+ this._list.scrollToIndex(0);
+ }
+ },
+
+ onContextMenu: function cv_onContextMenu(aEvent) {
+ let row = aEvent.target;
+ let text = ["msg", "href", "line", "code", "col"].map(function(attr) row.getAttribute(attr))
+ .filter(function(x) x).join("\n");
+
+ ContextMenuUI.showContextMenu({
+ target: row,
+ json: {
+ types: ["copy"],
+ string: text
+ }
+ });
+ },
+
+ onEvalKeyPress: function cv_onEvalKeyPress(aEvent) {
+ if (aEvent.keyCode == 13)
+ this.evaluateTypein();
+ },
+
+ onConsoleBoxKeyPress: function cv_onConsoleBoxKeyPress(aEvent) {
+ if ((aEvent.charCode == 99 || aEvent.charCode == 67) && aEvent.ctrlKey && this._list && this._list.selectedItem) {
+ let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
+ clipboard.copyString(this._list.selectedItem.getAttribute("msg"), document);
+ }
+ },
+
+ evaluateTypein: function cv_evaluateTypein() {
+ this._evalCode = this._evalTextbox.value;
+ this.loadOrDisplayResult();
+ },
+
+ loadOrDisplayResult: function cv_loadOrDisplayResult() {
+ if (this._evalCode) {
+ this._evalFrame.contentWindow.location = "javascript: " + this._evalCode.replace(/%/g, "%25");
+ this._evalCode = "";
+ return;
+ }
+
+ let resultRange = this._evalFrame.contentDocument.createRange();
+ resultRange.selectNode(this._evalFrame.contentDocument.documentElement);
+ let result = resultRange.toString();
+ if (result)
+ Services.console.logStringMessage(result);
+ // or could use appendMessage which doesn't persist
+ },
+
+ repeatChar: function cv_repeatChar(aChar, aCol) {
+ if (--aCol <= 0)
+ return "";
+
+ for (let i = 2; i < aCol; i += i)
+ aChar += aChar;
+
+ return aChar + aChar.slice(0, aCol - aChar.length);
+ }
+};
diff --git a/browser/metro/base/content/contenthandlers/ConsoleAPIObserver.js b/browser/metro/base/content/contenthandlers/ConsoleAPIObserver.js
new file mode 100644
index 000000000000..9fda40d45b3a
--- /dev/null
+++ b/browser/metro/base/content/contenthandlers/ConsoleAPIObserver.js
@@ -0,0 +1,121 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+dump("### ConsoleAPIObserver.js loaded\n");
+
+/*
+ * ConsoleAPIObserver
+ *
+ */
+
+var ConsoleAPIObserver = {
+ init: function init() {
+ addMessageListener("Browser:TabOpen", this);
+ addMessageListener("Browser:TabClose", this);
+ },
+
+ receiveMessage: function receiveMessage(aMessage) {
+ let json = aMessage.json;
+ switch (aMessage.name) {
+ case "Browser:TabOpen":
+ Services.obs.addObserver(this, "console-api-log-event", false);
+ break;
+ case "Browser:TabClose":
+ Services.obs.removeObserver(this, "console-api-log-event", false);
+ break;
+ }
+ },
+
+ observe: function observe(aMessage, aTopic, aData) {
+ let contentWindowId = content.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
+ aMessage = aMessage.wrappedJSObject;
+ if (aMessage.ID != contentWindowId)
+ return;
+
+ let mappedArguments = Array.map(aMessage.arguments, this.formatResult, this);
+ let joinedArguments = Array.join(mappedArguments, " ");
+
+ if (aMessage.level == "error" || aMessage.level == "warn") {
+ let flag = (aMessage.level == "error" ? Ci.nsIScriptError.errorFlag : Ci.nsIScriptError.warningFlag);
+ let consoleMsg = Cc["@mozilla.org/scripterror;1"].createInstance(Ci.nsIScriptError);
+ consoleMsg.init(joinedArguments, null, null, 0, 0, flag, "content javascript");
+ Services.console.logMessage(consoleMsg);
+ } else if (aMessage.level == "trace") {
+ let bundle = Services.strings.createBundle("chrome://global/locale/headsUpDisplay.properties");
+ let args = aMessage.arguments;
+ let filename = this.abbreviateSourceURL(args[0].filename);
+ let functionName = args[0].functionName || bundle.GetStringFromName("stacktrace.anonymousFunction");
+ let lineNumber = args[0].lineNumber;
+
+ let body = bundle.formatStringFromName("stacktrace.outputMessage", [filename, functionName, lineNumber], 3);
+ body += "\n";
+ args.forEach(function(aFrame) {
+ body += aFrame.filename + " :: " + aFrame.functionName + " :: " + aFrame.lineNumber + "\n";
+ });
+
+ Services.console.logStringMessage(body);
+ } else {
+ Services.console.logStringMessage(joinedArguments);
+ }
+ },
+
+ getResultType: function getResultType(aResult) {
+ let type = aResult === null ? "null" : typeof aResult;
+ if (type == "object" && aResult.constructor && aResult.constructor.name)
+ type = aResult.constructor.name;
+ return type.toLowerCase();
+ },
+
+ formatResult: function formatResult(aResult) {
+ let output = "";
+ let type = this.getResultType(aResult);
+ switch (type) {
+ case "string":
+ case "boolean":
+ case "date":
+ case "error":
+ case "number":
+ case "regexp":
+ output = aResult.toString();
+ break;
+ case "null":
+ case "undefined":
+ output = type;
+ break;
+ default:
+ if (aResult.toSource) {
+ try {
+ output = aResult.toSource();
+ } catch (ex) { }
+ }
+ if (!output || output == "({})") {
+ output = aResult.toString();
+ }
+ break;
+ }
+
+ return output;
+ },
+
+ abbreviateSourceURL: function abbreviateSourceURL(aSourceURL) {
+ // Remove any query parameters.
+ let hookIndex = aSourceURL.indexOf("?");
+ if (hookIndex > -1)
+ aSourceURL = aSourceURL.substring(0, hookIndex);
+
+ // Remove a trailing "/".
+ if (aSourceURL[aSourceURL.length - 1] == "/")
+ aSourceURL = aSourceURL.substring(0, aSourceURL.length - 1);
+
+ // Remove all but the last path component.
+ let slashIndex = aSourceURL.lastIndexOf("/");
+ if (slashIndex > -1)
+ aSourceURL = aSourceURL.substring(slashIndex + 1);
+
+ return aSourceURL;
+ }
+};
+
+ConsoleAPIObserver.init();
+
diff --git a/browser/metro/base/content/contenthandlers/Content.js b/browser/metro/base/content/contenthandlers/Content.js
new file mode 100644
index 000000000000..f2ddebed6f6f
--- /dev/null
+++ b/browser/metro/base/content/contenthandlers/Content.js
@@ -0,0 +1,696 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This stays here because otherwise it's hard to tell if there's a parsing error
+dump("### Content.js loaded\n");
+
+let Cc = Components.classes;
+let Ci = Components.interfaces;
+let Cu = Components.utils;
+let Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "Services", function() {
+ Cu.import("resource://gre/modules/Services.jsm");
+ return Services;
+});
+
+XPCOMUtils.defineLazyGetter(this, "Rect", function() {
+ Cu.import("resource://gre/modules/Geometry.jsm");
+ return Rect;
+});
+
+XPCOMUtils.defineLazyGetter(this, "Point", function() {
+ Cu.import("resource://gre/modules/Geometry.jsm");
+ return Point;
+});
+
+XPCOMUtils.defineLazyServiceGetter(this, "gFocusManager",
+ "@mozilla.org/focus-manager;1", "nsIFocusManager");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gDOMUtils",
+ "@mozilla.org/inspector/dom-utils;1", "inIDOMUtils");
+
+let XULDocument = Ci.nsIDOMXULDocument;
+let HTMLHtmlElement = Ci.nsIDOMHTMLHtmlElement;
+let HTMLIFrameElement = Ci.nsIDOMHTMLIFrameElement;
+let HTMLFrameElement = Ci.nsIDOMHTMLFrameElement;
+let HTMLFrameSetElement = Ci.nsIDOMHTMLFrameSetElement;
+let HTMLSelectElement = Ci.nsIDOMHTMLSelectElement;
+let HTMLOptionElement = Ci.nsIDOMHTMLOptionElement;
+
+const kReferenceDpi = 240; // standard "pixel" size used in some preferences
+
+const kStateActive = 0x00000001; // :active pseudoclass for elements
+
+/*
+ * ElementTouchHelper
+ *
+ * Assists users by watching for mouse clicks in content and redirect
+ * them to the best found target.
+ */
+const ElementTouchHelper = {
+ get radius() {
+ let prefs = Services.prefs;
+ delete this.radius;
+ return this.radius = { "top": prefs.getIntPref("ui.touch.radius.topmm"),
+ "right": prefs.getIntPref("ui.touch.radius.rightmm"),
+ "bottom": prefs.getIntPref("ui.touch.radius.bottommm"),
+ "left": prefs.getIntPref("ui.touch.radius.leftmm")
+ };
+ },
+
+ get weight() {
+ delete this.weight;
+ return this.weight = { "visited": Services.prefs.getIntPref("ui.touch.radius.visitedWeight")
+ };
+ },
+
+ /* Retrieve the closest element to a point by looking at borders position */
+ getClosest: function getClosest(aWindowUtils, aX, aY) {
+ if (!this.dpiRatio)
+ this.dpiRatio = aWindowUtils.displayDPI / kReferenceDpi;
+
+ let dpiRatio = this.dpiRatio;
+
+ let target = aWindowUtils.elementFromPoint(aX, aY,
+ true, /* ignore root scroll frame*/
+ false); /* don't flush layout */
+
+ // return early if the click is just over a clickable element
+ if (this._isElementClickable(target))
+ return target;
+
+ let nodes = aWindowUtils.nodesFromRect(aX, aY, this.radius.top * dpiRatio,
+ this.radius.right * dpiRatio,
+ this.radius.bottom * dpiRatio,
+ this.radius.left * dpiRatio, true, false);
+
+ let threshold = Number.POSITIVE_INFINITY;
+ for (let i = 0; i < nodes.length; i++) {
+ let current = nodes[i];
+ if (!current.mozMatchesSelector || !this._isElementClickable(current))
+ continue;
+
+ let rect = current.getBoundingClientRect();
+ let distance = this._computeDistanceFromRect(aX, aY, rect);
+
+ // increase a little bit the weight for already visited items
+ if (current && current.mozMatchesSelector("*:visited"))
+ distance *= (this.weight.visited / 100);
+
+ if (distance < threshold) {
+ target = current;
+ threshold = distance;
+ }
+ }
+
+ return target;
+ },
+
+ _isElementClickable: function _isElementClickable(aElement) {
+ const selector = "a,:link,:visited,[role=button],button,input,select,textarea,label";
+ for (let elem = aElement; elem; elem = elem.parentNode) {
+ if (this._hasMouseListener(elem))
+ return true;
+ if (elem.mozMatchesSelector && elem.mozMatchesSelector(selector))
+ return true;
+ }
+ return false;
+ },
+
+ _computeDistanceFromRect: function _computeDistanceFromRect(aX, aY, aRect) {
+ let x = 0, y = 0;
+ let xmost = aRect.left + aRect.width;
+ let ymost = aRect.top + aRect.height;
+
+ // compute horizontal distance from left/right border depending if X is
+ // before/inside/after the element's rectangle
+ if (aRect.left < aX && aX < xmost)
+ x = Math.min(xmost - aX, aX - aRect.left);
+ else if (aX < aRect.left)
+ x = aRect.left - aX;
+ else if (aX > xmost)
+ x = aX - xmost;
+
+ // compute vertical distance from top/bottom border depending if Y is
+ // above/inside/below the element's rectangle
+ if (aRect.top < aY && aY < ymost)
+ y = Math.min(ymost - aY, aY - aRect.top);
+ else if (aY < aRect.top)
+ y = aRect.top - aY;
+ if (aY > ymost)
+ y = aY - ymost;
+
+ return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2));
+ },
+
+ _els: Cc["@mozilla.org/eventlistenerservice;1"].getService(Ci.nsIEventListenerService),
+ _clickableEvents: ["mousedown", "mouseup", "click"],
+ _hasMouseListener: function _hasMouseListener(aElement) {
+ let els = this._els;
+ let listeners = els.getListenerInfoFor(aElement, {});
+ for (let i = 0; i < listeners.length; i++) {
+ if (this._clickableEvents.indexOf(listeners[i].type) != -1)
+ return true;
+ }
+ return false;
+ }
+};
+
+
+/*
+ * Global functions
+ */
+
+/*
+ * elementFromPoint
+ *
+ * @param x,y Browser coordinates
+ * @return Element at position, null if no active browser or no element found
+ */
+function elementFromPoint(x, y) {
+ // browser's elementFromPoint expect browser-relative client coordinates.
+ // subtract browser's scroll values to adjust
+ let cwu = Util.getWindowUtils(content);
+ let elem = ElementTouchHelper.getClosest(cwu, x, y);
+
+ // step through layers of IFRAMEs and FRAMES to find innermost element
+ while (elem && (elem instanceof HTMLIFrameElement ||
+ elem instanceof HTMLFrameElement)) {
+ // adjust client coordinates' origin to be top left of iframe viewport
+ let rect = elem.getBoundingClientRect();
+ x -= rect.left;
+ y -= rect.top;
+ let windowUtils = elem.contentDocument
+ .defaultView
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ elem = ElementTouchHelper.getClosest(windowUtils, x, y);
+ }
+
+ return elem;
+}
+
+/*
+ * getBoundingContentRect
+ *
+ * @param aElement
+ * @return Bounding content rect adjusted for scroll and frame offsets.
+ */
+function getBoundingContentRect(aElement) {
+ if (!aElement)
+ return new Rect(0, 0, 0, 0);
+
+ let document = aElement.ownerDocument;
+ while(document.defaultView.frameElement)
+ document = document.defaultView.frameElement.ownerDocument;
+
+ let offset = ContentScroll.getScrollOffset(content);
+ offset = new Point(offset.x, offset.y);
+
+ let r = aElement.getBoundingClientRect();
+
+ // step out of iframes and frames, offsetting scroll values
+ let view = aElement.ownerDocument.defaultView;
+ for (let frame = view; frame != content; frame = frame.parent) {
+ // adjust client coordinates' origin to be top left of iframe viewport
+ let rect = frame.frameElement.getBoundingClientRect();
+ let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth;
+ let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth;
+ offset.add(rect.left + parseInt(left), rect.top + parseInt(top));
+ }
+
+ return new Rect(r.left + offset.x, r.top + offset.y, r.width, r.height);
+}
+
+/*
+ * getOverflowContentBoundingRect
+ *
+ * @param aElement
+ * @return Bounding content rect adjusted for scroll and frame offsets.
+ */
+
+function getOverflowContentBoundingRect(aElement) {
+ let r = getBoundingContentRect(aElement);
+
+ // If the overflow is hidden don't bother calculating it
+ let computedStyle = aElement.ownerDocument.defaultView.getComputedStyle(aElement);
+ let blockDisplays = ["block", "inline-block", "list-item"];
+ if ((blockDisplays.indexOf(computedStyle.getPropertyValue("display")) != -1 &&
+ computedStyle.getPropertyValue("overflow") == "hidden") ||
+ aElement instanceof HTMLSelectElement) {
+ return r;
+ }
+
+ for (let i = 0; i < aElement.childElementCount; i++) {
+ r = r.union(getBoundingContentRect(aElement.children[i]));
+ }
+
+ return r;
+}
+
+/*
+ * Content
+ *
+ * Browser event receiver for content.
+ */
+let Content = {
+ _debugEvents: false,
+
+ get formAssistant() {
+ delete this.formAssistant;
+ return this.formAssistant = new FormAssistant();
+ },
+
+ init: function init() {
+ this._isZoomedToElement = false;
+
+ // Asyncronous messages sent from the browser
+ addMessageListener("Browser:Blur", this);
+ addMessageListener("Browser:SaveAs", this);
+ addMessageListener("Browser:MozApplicationCache:Fetch", this);
+ addMessageListener("Browser:SetCharset", this);
+ addMessageListener("Browser:CanUnload", this);
+ addMessageListener("Browser:PanBegin", this);
+
+ addEventListener("touchstart", this, false);
+ addEventListener("click", this, true);
+ addEventListener("keydown", this);
+ addEventListener("keyup", this);
+
+ // Synchronous events caught during the bubbling phase
+ addEventListener("MozApplicationManifest", this, false);
+ addEventListener("DOMContentLoaded", this, false);
+ addEventListener("pagehide", this, false);
+ // Attach a listener to watch for "click" events bubbling up from error
+ // pages and other similar page. This lets us fix bugs like 401575 which
+ // require error page UI to do privileged things, without letting error
+ // pages have any privilege themselves.
+ addEventListener("click", this, false);
+
+ docShell.QueryInterface(Ci.nsIDocShellHistory).useGlobalHistory = true;
+ },
+
+ /*******************************************
+ * Events
+ */
+
+ handleEvent: function handleEvent(aEvent) {
+ if (this._debugEvents) Util.dumpLn("Content:", aEvent.type);
+ switch (aEvent.type) {
+ case "MozApplicationManifest": {
+ let doc = aEvent.originalTarget;
+ sendAsyncMessage("Browser:MozApplicationManifest", {
+ location: doc.documentURIObject.spec,
+ manifest: doc.documentElement.getAttribute("manifest"),
+ charset: doc.characterSet
+ });
+ break;
+ }
+
+ case "keydown":
+ if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE)
+ this.formAssistant.close();
+ break;
+
+ case "keyup":
+ // If after a key is pressed we still have no input, then close
+ // the autocomplete. Perhaps the user used backspace or delete.
+ if (!aEvent.target.value)
+ this.formAssistant.close();
+ else
+ this.formAssistant.open(aEvent.target);
+ break;
+
+ case "click":
+ if (aEvent.eventPhase == aEvent.BUBBLING_PHASE)
+ this._onClick(aEvent);
+ else
+ this._genericMouseClick(aEvent);
+ break;
+
+ case "DOMContentLoaded":
+ this._maybeNotifyErroPage();
+ break;
+
+ case "pagehide":
+ if (aEvent.target == content.document)
+ this._resetFontSize();
+ break;
+
+ case "touchstart":
+ let touch = aEvent.changedTouches[0];
+ this._genericMouseDown(touch.clientX, touch.clientY);
+ break;
+ }
+ },
+
+ receiveMessage: function receiveMessage(aMessage) {
+ if (this._debugEvents) Util.dumpLn("Content:", aMessage.name);
+ let json = aMessage.json;
+ let x = json.x;
+ let y = json.y;
+ let modifiers = json.modifiers;
+
+ switch (aMessage.name) {
+ case "Browser:Blur":
+ gFocusManager.clearFocus(content);
+ break;
+
+ case "Browser:CanUnload":
+ let canUnload = docShell.contentViewer.permitUnload();
+ sendSyncMessage("Browser:CanUnload:Return", { permit: canUnload });
+ break;
+
+ case "Browser:SaveAs":
+ break;
+
+ case "Browser:MozApplicationCache:Fetch": {
+ let currentURI = Services.io.newURI(json.location, json.charset, null);
+ let manifestURI = Services.io.newURI(json.manifest, json.charset, currentURI);
+ let updateService = Cc["@mozilla.org/offlinecacheupdate-service;1"]
+ .getService(Ci.nsIOfflineCacheUpdateService);
+ updateService.scheduleUpdate(manifestURI, currentURI, content);
+ break;
+ }
+
+ case "Browser:SetCharset": {
+ docShell.charset = json.charset;
+
+ let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
+ webNav.reload(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
+ break;
+ }
+
+ case "Browser:PanBegin":
+ this._cancelTapHighlight();
+ break;
+ }
+ },
+
+ /******************************************************
+ * generic input handlers
+ *
+ * regardless of whether the input was received via
+ * message manager or sent directly via dispatch.
+ */
+
+ _genericMouseDown: function _genericMouseDown(x, y) {
+ let element = elementFromPoint(x, y);
+ if (!element)
+ return;
+
+ // There is no need to have a feedback for disabled element
+ let isDisabled = element instanceof HTMLOptionElement ?
+ (element.disabled || element.parentNode.disabled) : element.disabled;
+ if (isDisabled)
+ return;
+
+ // Set the target element to active
+ this._doTapHighlight(element);
+ },
+
+ _genericMouseClick: function _genericMouseClick(aEvent) {
+ ContextMenuHandler.reset();
+
+ let element = elementFromPoint(aEvent.clientX, aEvent.clientY);
+ if (!element)
+ return;
+
+ // Only show autocomplete after the item is clicked
+ if (!this.lastClickElement || this.lastClickElement != element) {
+ this.lastClickElement = element;
+ if (aEvent.mozInputSource == Ci.nsIDOMMouseEvent.MOZ_SOURCE_MOUSE &&
+ !(element instanceof HTMLSelectElement)) {
+ return;
+ }
+ }
+
+ this.formAssistant.focusSync = true;
+
+ // The form manager handles focus related changes on form elements.
+ // If it returns false, it didn't find anything to act on. If the
+ // target element doesn't match the current focus element, clear
+ // focus. This allows users to remove focus from form elements by
+ // taping on white space content.
+ if (!this.formAssistant.open(element, aEvent.clientX, aEvent.clientY)) {
+ if (gFocusManager.focusedElement &&
+ gFocusManager.focusedElement != element) {
+ gFocusManager.focusedElement.blur();
+ }
+ // This may not have any effect if element is unfocusable.
+ gFocusManager.setFocus(element, Ci.nsIFocusManager.FLAG_NOSCROLL);
+ sendAsyncMessage("FindAssist:Hide", { });
+ }
+
+ // Fire mouse events on everything but selects, see bug 685197
+ if (element instanceof HTMLSelectElement) {
+ aEvent.preventDefault()
+ aEvent.stopPropagation()
+ }
+ this._cancelTapHighlight();
+ this.formAssistant.focusSync = false;
+ },
+
+ /******************************************************
+ * Event handlers
+ */
+
+ // Checks clicks we care about - events bubbling up from about pages.
+ _onClick: function _onClick(aEvent) {
+ // Don't trust synthetic events
+ if (!aEvent.isTrusted)
+ return;
+
+ let ot = aEvent.originalTarget;
+ let errorDoc = ot.ownerDocument;
+ if (!errorDoc)
+ return;
+
+ // If the event came from an ssl error page, it is probably either
+ // "Add Exception…" or "Get me out of here!" button.
+ if (/^about:certerror\?e=nssBadCert/.test(errorDoc.documentURI)) {
+ let perm = errorDoc.getElementById("permanentExceptionButton");
+ let temp = errorDoc.getElementById("temporaryExceptionButton");
+ if (ot == temp || ot == perm) {
+ let action = (ot == perm ? "permanent" : "temporary");
+ sendAsyncMessage("Browser:CertException",
+ { url: errorDoc.location.href, action: action });
+ } else if (ot == errorDoc.getElementById("getMeOutOfHereButton")) {
+ sendAsyncMessage("Browser:CertException",
+ { url: errorDoc.location.href, action: "leave" });
+ }
+ } else if (/^about:blocked/.test(errorDoc.documentURI)) {
+ // The event came from a button on a malware/phishing block page
+ // First check whether it's malware or phishing, so that we can
+ // use the right strings/links.
+ let isMalware = /e=malwareBlocked/.test(errorDoc.documentURI);
+
+ if (ot == errorDoc.getElementById("getMeOutButton")) {
+ sendAsyncMessage("Browser:BlockedSite",
+ { url: errorDoc.location.href, action: "leave" });
+ } else if (ot == errorDoc.getElementById("reportButton")) {
+ // This is the "Why is this site blocked" button. For malware,
+ // we can fetch a site-specific report, for phishing, we redirect
+ // to the generic page describing phishing protection.
+ let action = isMalware ? "report-malware" : "report-phising";
+ sendAsyncMessage("Browser:BlockedSite",
+ { url: errorDoc.location.href, action: action });
+ } else if (ot == errorDoc.getElementById("ignoreWarningButton")) {
+ // Allow users to override and continue through to the site,
+ // but add a notify bar as a reminder, so that they don't lose
+ // track after, e.g., tab switching.
+ let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
+ webNav.loadURI(content.location,
+ Ci.nsIWebNavigation.LOAD_FLAGS_BYPASS_CLASSIFIER,
+ null, null, null);
+ }
+ }
+ },
+
+
+ /******************************************************
+ * General utilities
+ */
+
+ _getContentClientRects: function getContentClientRects(aElement) {
+ let offset = ContentScroll.getScrollOffset(content);
+ offset = new Point(offset.x, offset.y);
+
+ let nativeRects = aElement.getClientRects();
+ // step out of iframes and frames, offsetting scroll values
+ for (let frame = aElement.ownerDocument.defaultView; frame != content;
+ frame = frame.parent) {
+ // adjust client coordinates' origin to be top left of iframe viewport
+ let rect = frame.frameElement.getBoundingClientRect();
+ let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth;
+ let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth;
+ offset.add(rect.left + parseInt(left), rect.top + parseInt(top));
+ }
+
+ let result = [];
+ for (let i = nativeRects.length - 1; i >= 0; i--) {
+ let r = nativeRects[i];
+ result.push({ left: r.left + offset.x,
+ top: r.top + offset.y,
+ width: r.width,
+ height: r.height
+ });
+ }
+ return result;
+ },
+
+ _maybeNotifyErroPage: function _maybeNotifyErroPage() {
+ // Notify browser that an error page is being shown instead
+ // of the target location. Necessary to get proper thumbnail
+ // updates on chrome for error pages.
+ if (content.location.href !== content.document.documentURI)
+ sendAsyncMessage("Browser:ErrorPage", null);
+ },
+
+ _resetFontSize: function _resetFontSize() {
+ this._isZoomedToElement = false;
+ this._setMinFontSize(0);
+ },
+
+ _highlightElement: null,
+
+ _doTapHighlight: function _doTapHighlight(aElement) {
+ gDOMUtils.setContentState(aElement, kStateActive);
+ this._highlightElement = aElement;
+ },
+
+ _cancelTapHighlight: function _cancelTapHighlight(aElement) {
+ gDOMUtils.setContentState(content.document.documentElement, kStateActive);
+ this._highlightElement = null;
+ },
+
+ /*
+ * _sendMouseEvent
+ *
+ * Delivers mouse events directly to the content window, bypassing
+ * the input overlay.
+ */
+ _sendMouseEvent: function _sendMouseEvent(aName, aElement, aX, aY, aButton) {
+ // Elements can be off from the aX/aY point because due to touch radius.
+ // If outside, we move the touch point to the center of the element.
+ if (!(aElement instanceof HTMLHtmlElement)) {
+ let isTouchClick = true;
+ let rects = this._getContentClientRects(aElement);
+ for (let i = 0; i < rects.length; i++) {
+ let rect = rects[i];
+ // We might be able to deal with fractional pixels, but mouse
+ // events won't. Deflate the bounds in by 1 pixel to deal with
+ // any fractional scroll offset issues.
+ let inBounds =
+ (aX > rect.left + 1 && aX < (rect.left + rect.width - 1)) &&
+ (aY > rect.top + 1 && aY < (rect.top + rect.height - 1));
+ if (inBounds) {
+ isTouchClick = false;
+ break;
+ }
+ }
+
+ if (isTouchClick) {
+ let rect = new Rect(rects[0].left, rects[0].top,
+ rects[0].width, rects[0].height);
+ if (rect.isEmpty())
+ return;
+
+ let point = rect.center();
+ aX = point.x;
+ aY = point.y;
+ }
+ }
+
+ let button = aButton || 0;
+ let scrollOffset = ContentScroll.getScrollOffset(content);
+ let x = aX - scrollOffset.x;
+ let y = aY - scrollOffset.y;
+
+ // setting touch source here is important so that when this gets
+ // captured by our precise input detection we can ignore it.
+ let windowUtils = Util.getWindowUtils(content);
+ windowUtils.sendMouseEventToWindow(aName, x, y, button, 1, 0, true,
+ 1.0, Ci.nsIDOMMouseEvent.MOZ_SOURCE_MOUSE);
+ },
+
+ _setMinFontSize: function _setMinFontSize(aSize) {
+ let viewer = docShell.contentViewer.QueryInterface(Ci.nsIMarkupDocumentViewer);
+ if (viewer)
+ viewer.minFontSize = aSize;
+ }
+};
+
+Content.init();
+
+var FormSubmitObserver = {
+ init: function init(){
+ addMessageListener("Browser:TabOpen", this);
+ addMessageListener("Browser:TabClose", this);
+
+ addEventListener("pageshow", this, false);
+
+ Services.obs.addObserver(this, "invalidformsubmit", false);
+ },
+
+ handleEvent: function handleEvent(aEvent) {
+ let target = aEvent.originalTarget;
+ let isRootDocument = (target == content.document || target.ownerDocument == content.document);
+ if (!isRootDocument)
+ return;
+
+ // Reset invalid submit state on each pageshow
+ if (aEvent.type == "pageshow")
+ Content.formAssistant.invalidSubmit = false;
+ },
+
+ receiveMessage: function receiveMessage(aMessage) {
+ let json = aMessage.json;
+ switch (aMessage.name) {
+ case "Browser:TabOpen":
+ Services.obs.addObserver(this, "formsubmit", false);
+ break;
+ case "Browser:TabClose":
+ Services.obs.removeObserver(this, "formsubmit", false);
+ break;
+ }
+ },
+
+ notify: function notify(aFormElement, aWindow, aActionURI, aCancelSubmit) {
+ // Do not notify unless this is the window where the submit occurred
+ if (aWindow == content)
+ // We don't need to send any data along
+ sendAsyncMessage("Browser:FormSubmit", {});
+ },
+
+ notifyInvalidSubmit: function notifyInvalidSubmit(aFormElement, aInvalidElements) {
+ if (!aInvalidElements.length)
+ return;
+
+ let element = aInvalidElements.queryElementAt(0, Ci.nsISupports);
+ if (!(element instanceof HTMLInputElement ||
+ element instanceof HTMLTextAreaElement ||
+ element instanceof HTMLSelectElement ||
+ element instanceof HTMLButtonElement)) {
+ return;
+ }
+
+ Content.formAssistant.invalidSubmit = true;
+ Content.formAssistant.open(element);
+ },
+
+ QueryInterface : function(aIID) {
+ if (!aIID.equals(Ci.nsIFormSubmitObserver) &&
+ !aIID.equals(Ci.nsISupportsWeakReference) &&
+ !aIID.equals(Ci.nsISupports))
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ return this;
+ }
+};
+
+FormSubmitObserver.init();
diff --git a/browser/metro/base/content/contenthandlers/ContextMenuHandler.js b/browser/metro/base/content/contenthandlers/ContextMenuHandler.js
new file mode 100644
index 000000000000..d701d2be58fd
--- /dev/null
+++ b/browser/metro/base/content/contenthandlers/ContextMenuHandler.js
@@ -0,0 +1,360 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const kXLinkNamespace = "http://www.w3.org/1999/xlink";
+
+dump("### ContextMenuHandler.js loaded\n");
+
+var ContextMenuHandler = {
+ _types: [],
+
+ init: function ch_init() {
+ // Events we catch from content during the bubbling phase
+ addEventListener("contextmenu", this, false);
+ addEventListener("pagehide", this, false);
+ addEventListener("select", this, false);
+
+ // Messages we receive from browser
+ addMessageListener("Browser:ContextCommand", this, false);
+
+ this.popupNode = null;
+ },
+
+ _getLinkURL: function ch_getLinkURL(aLink) {
+ let href = aLink.href;
+ if (href)
+ return href;
+
+ href = aLink.getAttributeNS(kXLinkNamespace, "href");
+ if (!href || !href.match(/\S/)) {
+ // Without this we try to save as the current doc,
+ // for example, HTML case also throws if empty
+ throw "Empty href";
+ }
+
+ return Util.makeURLAbsolute(aLink.baseURI, href);
+ },
+
+ _getURI: function ch_getURI(aURL) {
+ try {
+ return Util.makeURI(aURL);
+ } catch (ex) { }
+
+ return null;
+ },
+
+ _getProtocol: function ch_getProtocol(aURI) {
+ if (aURI)
+ return aURI.scheme;
+ return null;
+ },
+
+ handleEvent: function ch_handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "contextmenu":
+ this._onContentContextMenu(aEvent);
+ break;
+ case "pagehide":
+ this.reset();
+ break;
+ case "select":
+ break;
+ }
+ },
+
+ receiveMessage: function ch_receiveMessage(aMessage) {
+ let node = this.popupNode;
+ let command = aMessage.json.command;
+
+ switch (command) {
+ case "play":
+ case "pause":
+ if (node instanceof Ci.nsIDOMHTMLMediaElement)
+ node[command]();
+ break;
+
+ case "videotab":
+ if (node instanceof Ci.nsIDOMHTMLVideoElement) {
+ node.pause();
+ Cu.import("resource:///modules/video.jsm");
+ Video.fullScreenSourceElement = node;
+ sendAsyncMessage("Browser:FullScreenVideo:Start");
+ }
+ break;
+
+ case "select-all":
+ this._onSelectAll();
+ break;
+
+ case "paste":
+ this._onPaste();
+ break;
+ }
+ },
+
+ /******************************************************
+ * Event handlers
+ */
+
+ reset: function ch_reset() {
+ this.popupNode = null;
+ this._target = null;
+ },
+
+ // content contextmenu
+ _onContentContextMenu: function _onContentContextMenu(aEvent) {
+ if (aEvent.defaultPrevented)
+ return;
+
+ // Don't let these bubble up to input.js
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+
+ let state = {
+ types: [],
+ label: "",
+ linkURL: "",
+ linkTitle: "",
+ linkProtocol: null,
+ mediaURL: "",
+ x: aEvent.x,
+ y: aEvent.y
+ };
+
+ let popupNode = this.popupNode = aEvent.originalTarget;
+ let imageUrl = "";
+
+ // Do checks for nodes that never have children.
+ if (popupNode.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
+ // See if the user clicked on an image.
+ if (popupNode instanceof Ci.nsIImageLoadingContent && popupNode.currentURI) {
+ state.types.push("image");
+ state.label = state.mediaURL = popupNode.currentURI.spec;
+ imageUrl = state.mediaURL;
+
+ // Retrieve the type of image from the cache since the url can fail to
+ // provide valuable informations
+ try {
+ let imageCache = Cc["@mozilla.org/image/cache;1"].getService(Ci.imgICache);
+ let props = imageCache.findEntryProperties(popupNode.currentURI,
+ content.document.characterSet);
+ if (props) {
+ state.contentType = String(props.get("type", Ci.nsISupportsCString));
+ state.contentDisposition = String(props.get("content-disposition",
+ Ci.nsISupportsCString));
+ }
+ } catch (ex) {
+ Util.dumpLn(ex.message);
+ // Failure to get type and content-disposition off the image is non-fatal
+ }
+ }
+ }
+
+ let elem = popupNode;
+ let isText = false;
+
+ while (elem) {
+ if (elem.nodeType == Ci.nsIDOMNode.ELEMENT_NODE) {
+ // is the target a link or a descendant of a link?
+ if (this._isLink(elem)) {
+ // If this is an image that links to itself, don't include both link and
+ // image otpions.
+ if (imageUrl == this._getLinkURL(elem)) {
+ elem = elem.parentNode;
+ continue;
+ }
+
+ state.types.push("link");
+ state.label = state.linkURL = this._getLinkURL(elem);
+ linkUrl = state.linkURL;
+ state.linkTitle = popupNode.textContent || popupNode.title;
+ state.linkProtocol = this._getProtocol(this._getURI(state.linkURL));
+ break;
+ } else if (this._isTextInput(elem)) {
+ let selectionStart = elem.selectionStart;
+ let selectionEnd = elem.selectionEnd;
+
+ state.types.push("input-text");
+ this._target = elem;
+
+ // Don't include "copy" for password fields.
+ if (!(elem instanceof Ci.nsIDOMHTMLInputElement) || elem.mozIsTextField(true)) {
+ if (selectionStart != selectionEnd) {
+ state.types.push("copy");
+ state.string = elem.value.slice(selectionStart, selectionEnd);
+ } else if (elem.value) {
+ state.types.push("copy-all");
+ state.string = elem.value;
+ }
+ }
+
+ if (selectionStart > 0 || selectionEnd < elem.textLength) {
+ state.types.push("select-all");
+ }
+
+ if (!elem.textLength) {
+ state.types.push("input-empty");
+ }
+
+ let flavors = ["text/unicode"];
+ let cb = Cc["@mozilla.org/widget/clipboard;1"].getService(Ci.nsIClipboard);
+ let hasData = cb.hasDataMatchingFlavors(flavors,
+ flavors.length,
+ Ci.nsIClipboard.kGlobalClipboard);
+ if (hasData && !elem.readOnly) {
+ state.types.push("paste");
+ }
+ break;
+ } else if (this._isText(elem)) {
+ isText = true;
+ } else if (elem instanceof Ci.nsIDOMHTMLMediaElement ||
+ elem instanceof Ci.nsIDOMHTMLVideoElement) {
+ state.label = state.mediaURL = (elem.currentSrc || elem.src);
+ state.types.push((elem.paused || elem.ended) ?
+ "media-paused" : "media-playing");
+ if (elem instanceof Ci.nsIDOMHTMLVideoElement) {
+ state.types.push("video");
+ }
+ }
+ }
+
+ elem = elem.parentNode;
+ }
+
+ if (isText) {
+ // If this is text and has a selection, we want to bring
+ // up the copy option on the context menu.
+ if (content && content.getSelection() &&
+ content.getSelection().toString().length > 0) {
+ state.string = content.getSelection().toString();
+ state.types.push("copy");
+ state.types.push("selected-text");
+ } else {
+ // Add general content text if this isn't anything specific
+ if (state.types.indexOf("image") == -1 &&
+ state.types.indexOf("media") == -1 &&
+ state.types.indexOf("video") == -1 &&
+ state.types.indexOf("link") == -1 &&
+ state.types.indexOf("input-text") == -1) {
+ state.types.push("content-text");
+ }
+ }
+ }
+
+ // populate position and event source
+ state.xPos = aEvent.clientX;
+ state.yPos = aEvent.clientY;
+ state.source = aEvent.mozInputSource;
+
+ for (let i = 0; i < this._types.length; i++)
+ if (this._types[i].handler(state, popupNode))
+ state.types.push(this._types[i].name);
+
+ sendAsyncMessage("Content:ContextMenu", state);
+ },
+
+ _onSelectAll: function _onSelectAll() {
+ if (this._isTextInput(this._target)) {
+ // select all text in the input control
+ this._target.select();
+ } else {
+ // select the entire document
+ content.getSelection().selectAllChildren(content.document);
+ }
+ this.reset();
+ },
+
+ _onPaste: function _onPaste() {
+ // paste text if this is an input control
+ if (this._isTextInput(this._target)) {
+ let edit = this._target.QueryInterface(Ci.nsIDOMNSEditableElement);
+ if (edit) {
+ edit.editor.paste(Ci.nsIClipboard.kGlobalClipboard);
+ } else {
+ Util.dumpLn("error: target element does not support nsIDOMNSEditableElement");
+ }
+ }
+ this.reset();
+ },
+
+ /*
+ * Utility routines used in testing for various
+ * HTML element types.
+ */
+
+ _isTextInput: function _isTextInput(element) {
+ return ((element instanceof Ci.nsIDOMHTMLInputElement &&
+ element.mozIsTextField(false)) ||
+ element instanceof Ci.nsIDOMHTMLTextAreaElement);
+ },
+
+ _isLink: function _isLink(element) {
+ return ((element instanceof Ci.nsIDOMHTMLAnchorElement && element.href) ||
+ (element instanceof Ci.nsIDOMHTMLAreaElement && element.href) ||
+ element instanceof Ci.nsIDOMHTMLLinkElement ||
+ element.getAttributeNS(kXLinkNamespace, "type") == "simple");
+ },
+
+ _isText: function _isText(element) {
+ return (element instanceof Ci.nsIDOMHTMLParagraphElement ||
+ element instanceof Ci.nsIDOMHTMLDivElement ||
+ element instanceof Ci.nsIDOMHTMLLIElement ||
+ element instanceof Ci.nsIDOMHTMLPreElement ||
+ element instanceof Ci.nsIDOMHTMLHeadingElement ||
+ element instanceof Ci.nsIDOMHTMLTableCellElement ||
+ element instanceof Ci.nsIDOMHTMLBodyElement);
+ },
+
+ /**
+ * For add-ons to add new types and data to the ContextMenu message.
+ *
+ * @param aName A string to identify the new type.
+ * @param aHandler A function that takes a state object and a target element.
+ * If aHandler returns true, then aName will be added to the list of types.
+ * The function may also modify the state object.
+ */
+ registerType: function registerType(aName, aHandler) {
+ this._types.push({name: aName, handler: aHandler});
+ },
+
+ /** Remove all handlers registered for a given type. */
+ unregisterType: function unregisterType(aName) {
+ this._types = this._types.filter(function(type) type.name != aName);
+ }
+};
+
+ContextMenuHandler.init();
+
+ContextMenuHandler.registerType("mailto", function(aState, aElement) {
+ return aState.linkProtocol == "mailto";
+});
+
+ContextMenuHandler.registerType("callto", function(aState, aElement) {
+ let protocol = aState.linkProtocol;
+ return protocol == "tel" || protocol == "callto" || protocol == "sip" || protocol == "voipto";
+});
+
+ContextMenuHandler.registerType("link-openable", function(aState, aElement) {
+ return Util.isOpenableScheme(aState.linkProtocol);
+});
+
+["image", "video"].forEach(function(aType) {
+ ContextMenuHandler.registerType(aType+"-shareable", function(aState, aElement) {
+ if (aState.types.indexOf(aType) == -1)
+ return false;
+
+ let protocol = ContextMenuHandler._getProtocol(ContextMenuHandler._getURI(aState.mediaURL));
+ return Util.isShareableScheme(protocol);
+ });
+});
+
+ContextMenuHandler.registerType("image-loaded", function(aState, aElement) {
+ if (aState.types.indexOf("image") != -1) {
+ let request = aElement.getRequest(Ci.nsIImageLoadingContent.CURRENT_REQUEST);
+ if (request && (request.imageStatus & request.STATUS_SIZE_AVAILABLE))
+ return true;
+ }
+ return false;
+});
+
diff --git a/browser/metro/base/content/contenthandlers/FindHandler.js b/browser/metro/base/content/contenthandlers/FindHandler.js
new file mode 100644
index 000000000000..a72b33b39ff3
--- /dev/null
+++ b/browser/metro/base/content/contenthandlers/FindHandler.js
@@ -0,0 +1,76 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+dump("### FindHandler.js loaded\n");
+
+var FindHandler = {
+ get _fastFind() {
+ delete this._fastFind;
+ this._fastFind = Cc["@mozilla.org/typeaheadfind;1"].createInstance(Ci.nsITypeAheadFind);
+ this._fastFind.init(docShell);
+ return this._fastFind;
+ },
+
+ init: function findHandlerInit() {
+ addMessageListener("FindAssist:Find", this);
+ addMessageListener("FindAssist:Next", this);
+ addMessageListener("FindAssist:Previous", this);
+ },
+
+ receiveMessage: function findHandlerReceiveMessage(aMessage) {
+ let findResult = Ci.nsITypeAheadFind.FIND_NOTFOUND;
+ let json = aMessage.json;
+ switch (aMessage.name) {
+ case "FindAssist:Find":
+ findResult = this._fastFind.find(json.searchString, false);
+ break;
+
+ case "FindAssist:Previous":
+ findResult = this._fastFind.findAgain(true, false);
+ break;
+
+ case "FindAssist:Next":
+ findResult = this._fastFind.findAgain(false, false);
+ break;
+ }
+
+ if (findResult == Ci.nsITypeAheadFind.FIND_NOTFOUND) {
+ sendAsyncMessage("FindAssist:Show", { rect: null , result: findResult });
+ return;
+ }
+
+ let selection = this._fastFind.currentWindow.getSelection();
+ if (!selection.rangeCount || selection.isCollapsed) {
+ // The selection can be into an input or a textarea element
+ let nodes = content.document.querySelectorAll("input[type='text'], textarea");
+ for (let i = 0; i < nodes.length; i++) {
+ let node = nodes[i];
+ if (node instanceof Ci.nsIDOMNSEditableElement && node.editor) {
+ selection = node.editor.selectionController.getSelection(Ci.nsISelectionController.SELECTION_NORMAL);
+ if (selection.rangeCount && !selection.isCollapsed)
+ break;
+ }
+ }
+ }
+
+ let scroll = ContentScroll.getScrollOffset(content);
+ for (let frame = this._fastFind.currentWindow; frame != content; frame = frame.parent) {
+ let rect = frame.frameElement.getBoundingClientRect();
+ let left = frame.getComputedStyle(frame.frameElement, "").borderLeftWidth;
+ let top = frame.getComputedStyle(frame.frameElement, "").borderTopWidth;
+ scroll.add(rect.left + parseInt(left), rect.top + parseInt(top));
+ }
+
+ let rangeRect = selection.getRangeAt(0).getBoundingClientRect();
+ let rect = new Rect(scroll.x + rangeRect.left, scroll.y + rangeRect.top, rangeRect.width, rangeRect.height);
+
+ // Ensure the potential "scroll" event fired during a search as already fired
+ let timer = new Util.Timeout(function() {
+ sendAsyncMessage("FindAssist:Show", { rect: rect.isEmpty() ? null: rect , result: findResult });
+ });
+ timer.once(0);
+ }
+};
+
+FindHandler.init();
diff --git a/browser/metro/base/content/contenthandlers/FormHelper.js b/browser/metro/base/content/contenthandlers/FormHelper.js
new file mode 100644
index 000000000000..6ee0bacea6ad
--- /dev/null
+++ b/browser/metro/base/content/contenthandlers/FormHelper.js
@@ -0,0 +1,972 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+let Ci = Components.interfaces;
+let Cc = Components.classes;
+
+dump("### FormHelper.js loaded\n");
+
+const kPrefFormHelperEnabled = "formhelper.enabled";
+
+let HTMLTextAreaElement = Ci.nsIDOMHTMLTextAreaElement;
+let HTMLInputElement = Ci.nsIDOMHTMLInputElement;
+let HTMLSelectElement = Ci.nsIDOMHTMLSelectElement;
+let HTMLIFrameElement = Ci.nsIDOMHTMLIFrameElement;
+let HTMLDocument = Ci.nsIDOMHTMLDocument;
+let HTMLHtmlElement = Ci.nsIDOMHTMLHtmlElement;
+let HTMLBodyElement = Ci.nsIDOMHTMLBodyElement;
+let HTMLLabelElement = Ci.nsIDOMHTMLLabelElement;
+let HTMLButtonElement = Ci.nsIDOMHTMLButtonElement;
+let HTMLOptGroupElement = Ci.nsIDOMHTMLOptGroupElement;
+let HTMLOptionElement = Ci.nsIDOMHTMLOptionElement;
+let XULMenuListElement = Ci.nsIDOMXULMenuListElement;
+
+/**
+ * Responsible of navigation between forms fields and of the opening of the assistant
+ */
+function FormAssistant() {
+ addMessageListener("FormAssist:Closed", this);
+ addMessageListener("FormAssist:Previous", this);
+ addMessageListener("FormAssist:Next", this);
+ addMessageListener("FormAssist:ChoiceSelect", this);
+ addMessageListener("FormAssist:ChoiceChange", this);
+ addMessageListener("FormAssist:AutoComplete", this);
+ addMessageListener("Content:SetWindowSize", this);
+
+ /* Listen text events in order to update the autocomplete suggestions as soon
+ * a key is entered on device
+ */
+ addEventListener("text", this, false);
+
+ addEventListener("keypress", this, true);
+ addEventListener("keyup", this, false);
+ addEventListener("focus", this, true);
+ addEventListener("blur", this, true);
+ addEventListener("pageshow", this, false);
+ addEventListener("pagehide", this, false);
+ addEventListener("submit", this, false);
+
+ this._enabled = Services.prefs.prefHasUserValue(kPrefFormHelperEnabled) ?
+ Services.prefs.getBoolPref(kPrefFormHelperEnabled) : false;
+};
+
+FormAssistant.prototype = {
+ _debugEvents: false,
+ _selectWrapper: null,
+ _currentIndex: -1,
+ _elements: [],
+
+ invalidSubmit: false,
+
+ get currentElement() {
+ return this._elements[this._currentIndex];
+ },
+
+ get currentIndex() {
+ return this._currentIndex;
+ },
+
+ set currentIndex(aIndex) {
+ let element = this._elements[aIndex];
+ if (!element)
+ return -1;
+
+ if (this._isVisibleElement(element)) {
+ this._currentIndex = aIndex;
+ gFocusManager.setFocus(element, Ci.nsIFocusManager.FLAG_NOSCROLL);
+
+ // To ensure we get the current caret positionning of the focused
+ // element we need to delayed a bit the event
+ this._executeDelayed(function(self) {
+ // Bug 640870
+ // Sometimes the element inner frame get destroyed while the element
+ // receive the focus because the display is turned to 'none' for
+ // example, in this "fun" case just do nothing if the element is hidden
+ if (self._isVisibleElement(gFocusManager.focusedElement))
+ sendAsyncMessage("FormAssist:Show", self._getJSON());
+ });
+ } else {
+ // Repopulate the list of elements in the page, some could have gone
+ // because of AJAX changes for example
+ this._elements = [];
+ let currentIndex = this._getAllElements(gFocusManager.focusedElement)
+
+ if (aIndex < this._currentIndex)
+ this.currentIndex = currentIndex - 1;
+ else if (aIndex > this._currentIndex)
+ this.currentIndex = currentIndex + 1;
+ else if (this._currentIndex != currentIndex)
+ this.currentIndex = currentIndex;
+ }
+ return element;
+ },
+
+ _open: false,
+ open: function formHelperOpen(aElement, aX, aY) {
+ // If the click is on an option element we want to check if the parent
+ // is a valid target.
+ if (aElement instanceof HTMLOptionElement &&
+ aElement.parentNode instanceof HTMLSelectElement &&
+ !aElement.disabled) {
+ aElement = aElement.parentNode;
+ }
+
+ // The form assistant will close if a click happen:
+ // * outside of the scope of the form helper
+ // * hover a button of type=[image|submit]
+ // * hover a disabled element
+ if (!this._isValidElement(aElement)) {
+ let passiveButtons = { button: true, checkbox: true, file: true, radio: true, reset: true };
+ if ((aElement instanceof HTMLInputElement || aElement instanceof HTMLButtonElement) &&
+ passiveButtons[aElement.type] && !aElement.disabled)
+ return false;
+
+ // Check for plugins element
+ if (aElement instanceof Ci.nsIDOMHTMLEmbedElement) {
+ aX = aX || 0;
+ aY = aY || 0;
+ this._executeDelayed(function(self) {
+ let utils = Util.getWindowUtils(aElement.ownerDocument.defaultView);
+ if (utils.IMEStatus == utils.IME_STATUS_PLUGIN) {
+ let jsvar = {
+ current: {
+ id: aElement.id,
+ name: aElement.name,
+ title: "plugin",
+ value: null,
+ maxLength: 0,
+ type: (aElement.getAttribute("type") || "").toLowerCase(),
+ choices: null,
+ isAutocomplete: false,
+ validationMessage: null,
+ list: null,
+ rect: getBoundingContentRect(aElement),
+ caretRect: new Rect(aX, aY, 1, 10),
+ editable: true
+ },
+ hasPrevious: false,
+ hasNext: false
+ };
+ sendAsyncMessage("FormAssist:Show", jsvar);
+ }
+ });
+ return false;
+ }
+ return this.close();
+ }
+
+ // Look for a top editable element
+ if (this._isEditable(aElement))
+ aElement = this._getTopLevelEditable(aElement);
+
+ // There are some cases where we still want data to be sent to FormHelperUI
+ // even if form assistant is disabled:
+ // - the element is a choice list
+ // - the element has autocomplete suggestions
+ this._enabled = Services.prefs.prefHasUserValue(kPrefFormHelperEnabled) ?
+ Services.prefs.getBoolPref(kPrefFormHelperEnabled) : false;
+ if (!this._enabled && !this._isSelectElement(aElement) && !this._isAutocomplete(aElement)) {
+ return this.close();
+ }
+
+ if (this._enabled) {
+ this._elements = [];
+ this.currentIndex = this._getAllElements(aElement);
+ } else {
+ this._elements = [aElement];
+ this.currentIndex = 0;
+ }
+
+ return this._open = true;
+ },
+
+ close: function close() {
+ if (this._open) {
+ this._currentIndex = -1;
+ this._elements = [];
+ sendAsyncMessage("FormAssist:Hide", { });
+ this._open = false;
+ }
+
+ return this._open;
+ },
+
+ receiveMessage: function receiveMessage(aMessage) {
+ if (this._debugEvents) Util.dumpLn(aMessage.name);
+ let currentElement = this.currentElement;
+ if ((!this._enabled && !this._isAutocomplete(currentElement) && !getWrapperForElement(currentElement)) || !currentElement)
+ return;
+
+ let json = aMessage.json;
+ switch (aMessage.name) {
+ case "FormAssist:Previous":
+ this.currentIndex--;
+ break;
+
+ case "FormAssist:Next":
+ this.currentIndex++;
+ break;
+
+ case "Content:SetWindowSize":
+ // If the CSS viewport change just show the current element to the new
+ // position
+ sendAsyncMessage("FormAssist:Resize", this._getJSON());
+ break;
+
+ case "FormAssist:ChoiceSelect": {
+ this._selectWrapper = getWrapperForElement(currentElement);
+ this._selectWrapper.select(json.index, json.selected);
+ break;
+ }
+
+ case "FormAssist:ChoiceChange": {
+ // ChoiceChange could happened once we have move to an other element or
+ // to nothing, so we should keep the used wrapper in mind
+ this._selectWrapper.fireOnChange();
+
+ // New elements can be shown when a select is updated so we need to
+ // reconstruct the inner elements array and to take care of possible
+ // focus change, this is why we use "self.currentElement" instead of
+ // using directly "currentElement".
+ this._executeDelayed(function(self) {
+ let currentElement = self.currentElement;
+ if (!currentElement)
+ return;
+
+ self._elements = [];
+ self._currentIndex = self._getAllElements(currentElement);
+ });
+ break;
+ }
+
+ case "FormAssist:AutoComplete": {
+ try {
+ currentElement = currentElement.QueryInterface(Ci.nsIDOMNSEditableElement);
+ let imeEditor = currentElement.editor.QueryInterface(Ci.nsIEditorIMESupport);
+ if (imeEditor.composing)
+ imeEditor.forceCompositionEnd();
+ }
+ catch(e) {}
+
+ currentElement.value = json.value;
+
+ let event = currentElement.ownerDocument.createEvent("Events");
+ event.initEvent("DOMAutoComplete", true, true);
+ currentElement.dispatchEvent(event);
+ break;
+ }
+
+ case "FormAssist:Closed":
+ currentElement.blur();
+ this._currentIndex = null;
+ this._open = false;
+ break;
+ }
+ },
+
+ _els: Cc["@mozilla.org/eventlistenerservice;1"].getService(Ci.nsIEventListenerService),
+ _hasKeyListener: function _hasKeyListener(aElement) {
+ let els = this._els;
+ let listeners = els.getListenerInfoFor(aElement, {});
+ for (let i = 0; i < listeners.length; i++) {
+ let listener = listeners[i];
+ if (["keyup", "keydown", "keypress"].indexOf(listener.type) != -1
+ && !listener.inSystemEventGroup) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ focusSync: false,
+ handleEvent: function formHelperHandleEvent(aEvent) {
+ if (this._debugEvents) Util.dumpLn(aEvent.type, this.currentElement);
+ // focus changes should be taken into account only if the user has done a
+ // manual operation like manually clicking
+ let shouldIgnoreFocus = (aEvent.type == "focus" && !this._open && !this.focusSync);
+ if ((!this._open && aEvent.type != "focus") || shouldIgnoreFocus) {
+ return;
+ }
+
+ let currentElement = this.currentElement;
+ switch (aEvent.type) {
+ case "submit":
+ // submit is a final action and the form assistant should be closed
+ this.close();
+ break;
+
+ case "pagehide":
+ case "pageshow":
+ // When reacting to a page show/hide, if the focus is different this
+ // could mean the web page has dramatically changed because of
+ // an Ajax change based on fragment identifier
+ if (gFocusManager.focusedElement != currentElement)
+ this.close();
+ break;
+
+ case "focus":
+ let focusedElement = gFocusManager.getFocusedElementForWindow(content, true, {}) || aEvent.target;
+
+ // If a body element is editable and the body is the child of an
+ // iframe we can assume this is an advanced HTML editor, so let's
+ // redirect the form helper selection to the iframe element
+ if (focusedElement && this._isEditable(focusedElement)) {
+ let editableElement = this._getTopLevelEditable(focusedElement);
+ if (this._isValidElement(editableElement)) {
+ this._executeDelayed(function(self) {
+ self.open(editableElement);
+ });
+ }
+ return;
+ }
+
+ // if an element is focused while we're closed but the element can be handle
+ // by the assistant, try to activate it (only during mouseup)
+ if (!currentElement) {
+ if (focusedElement && this._isValidElement(focusedElement)) {
+ this._executeDelayed(function(self) {
+ self.open(focusedElement);
+ });
+ }
+ return;
+ }
+
+ let focusedIndex = this._getIndexForElement(focusedElement);
+ if (focusedIndex != -1 && this.currentIndex != focusedIndex)
+ this.currentIndex = focusedIndex;
+ break;
+
+ case "blur":
+ content.setTimeout(function(self) {
+ if (!self._open)
+ return;
+
+ // If the blurring causes focus be in no other element,
+ // we should close the form assistant.
+ let focusedElement = gFocusManager.getFocusedElementForWindow(content, true, {});
+ if (!focusedElement)
+ self.close();
+ }, 0, this);
+ break;
+
+ case "text":
+ if (this._isValidatable(aEvent.target))
+ sendAsyncMessage("FormAssist:ValidationMessage", this._getJSON());
+
+ if (this._isAutocomplete(aEvent.target))
+ sendAsyncMessage("FormAssist:AutoComplete", this._getJSON());
+ break;
+
+ // key processing inside a select element are done during the keypress
+ // handler, preventing this one to be fired cancel the selection change
+ case "keypress":
+ // There is no need to handle keys if there is not element currently
+ // used by the form assistant
+ if (!currentElement)
+ return;
+
+ let formExceptions = { button: true, checkbox: true, file: true, image: true, radio: true, reset: true, submit: true };
+ if (this._isSelectElement(currentElement) || formExceptions[currentElement.type] ||
+ currentElement instanceof HTMLButtonElement || (currentElement.getAttribute("role") == "button" && currentElement.hasAttribute("tabindex"))) {
+ switch (aEvent.keyCode) {
+ case aEvent.DOM_VK_RIGHT:
+ this._executeDelayed(function(self) {
+ self.currentIndex++;
+ });
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ break;
+
+ case aEvent.DOM_VK_LEFT:
+ this._executeDelayed(function(self) {
+ self.currentIndex--;
+ });
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ break;
+ }
+ }
+ break;
+
+ case "keyup":
+ // There is no need to handle keys if there is not element currently
+ // used by the form assistant
+ if (!currentElement)
+ return;
+
+ switch (aEvent.keyCode) {
+ case aEvent.DOM_VK_DOWN:
+ if (currentElement instanceof HTMLInputElement && !this._isAutocomplete(currentElement)) {
+ if (this._hasKeyListener(currentElement))
+ return;
+ } else if (currentElement instanceof HTMLTextAreaElement) {
+ let existSelection = currentElement.selectionEnd - currentElement.selectionStart;
+ let isEnd = (currentElement.textLength == currentElement.selectionEnd);
+ if (!isEnd || existSelection)
+ return;
+ } else if (getListForElement(currentElement)) {
+ this.currentIndex = this.currentIndex;
+ return;
+ }
+
+ this.currentIndex++;
+ break;
+
+ case aEvent.DOM_VK_UP:
+ if (currentElement instanceof HTMLInputElement && !this._isAutocomplete(currentElement)) {
+ if (this._hasKeyListener(currentElement))
+ return;
+ } else if (currentElement instanceof HTMLTextAreaElement) {
+ let existSelection = currentElement.selectionEnd - currentElement.selectionStart;
+ let isStart = (currentElement.selectionEnd == 0);
+ if (!isStart || existSelection)
+ return;
+ } else if (this._isSelectElement(currentElement)) {
+ this.currentIndex = this.currentIndex;
+ return;
+ }
+
+ this.currentIndex--;
+ break;
+
+ case aEvent.DOM_VK_RETURN:
+ if (!this._isVisibleElement(currentElement))
+ this.close();
+ break;
+
+ case aEvent.DOM_VK_ESCAPE:
+ case aEvent.DOM_VK_TAB:
+ break;
+
+ default:
+ if (this._isValidatable(aEvent.target)) {
+ sendAsyncMessage("FormAssist:ValidationMessage", this._getJSON());
+ }
+
+ if (this._isAutocomplete(aEvent.target)) {
+ sendAsyncMessage("FormAssist:AutoComplete", this._getJSON());
+ } else if (currentElement && this._isSelectElement(currentElement)) {
+ this.currentIndex = this.currentIndex;
+ }
+ break;
+ }
+
+ let caretRect = this._getCaretRect();
+ if (!caretRect.isEmpty())
+ sendAsyncMessage("FormAssist:Update", { caretRect: caretRect });
+ }
+ },
+
+ _executeDelayed: function formHelperExecuteSoon(aCallback) {
+ let self = this;
+ let timer = new Util.Timeout(function() {
+ aCallback(self);
+ });
+ timer.once(0);
+ },
+
+ _filterEditables: function formHelperFilterEditables(aNodes) {
+ let result = [];
+ for (let i = 0; i < aNodes.length; i++) {
+ let node = aNodes[i];
+
+ // Avoid checking the top level editable element of each node
+ if (this._isEditable(node)) {
+ let editableElement = this._getTopLevelEditable(node);
+ if (result.indexOf(editableElement) == -1)
+ result.push(editableElement);
+ }
+ else {
+ result.push(node);
+ }
+ }
+ return result;
+ },
+
+ _isEditable: function formHelperIsEditable(aElement) {
+ let canEdit = false;
+
+ if (aElement.isContentEditable || aElement.designMode == "on") {
+ canEdit = true;
+ } else if (aElement instanceof HTMLIFrameElement && (aElement.contentDocument.body.isContentEditable || aElement.contentDocument.designMode == "on")) {
+ canEdit = true;
+ } else {
+ canEdit = aElement.ownerDocument && aElement.ownerDocument.designMode == "on";
+ }
+
+ return canEdit;
+ },
+
+ _getTopLevelEditable: function formHelperGetTopLevelEditable(aElement) {
+ if (!(aElement instanceof HTMLIFrameElement)) {
+ let element = aElement;
+
+ // Retrieve the top element that is editable
+ if (element instanceof HTMLHtmlElement)
+ element = element.ownerDocument.body;
+ else if (element instanceof HTMLDocument)
+ element = element.body;
+
+ while (element && !this._isEditable(element))
+ element = element.parentNode;
+
+ // Return the container frame if we are into a nested editable frame
+ if (element && element instanceof HTMLBodyElement && element.ownerDocument.defaultView != content.document.defaultView)
+ return element.ownerDocument.defaultView.frameElement;
+ }
+
+ return aElement;
+ },
+
+ _isValidatable: function(aElement) {
+ return this.invalidSubmit &&
+ (aElement instanceof HTMLInputElement ||
+ aElement instanceof HTMLTextAreaElement ||
+ aElement instanceof HTMLSelectElement ||
+ aElement instanceof HTMLButtonElement);
+ },
+
+ _isAutocomplete: function formHelperIsAutocomplete(aElement) {
+ if (aElement instanceof HTMLInputElement) {
+ if (aElement.getAttribute("type") == "password")
+ return false;
+
+ let autocomplete = aElement.getAttribute("autocomplete");
+ let allowedValues = ["off", "false", "disabled"];
+ if (allowedValues.indexOf(autocomplete) == -1)
+ return true;
+ }
+
+ return false;
+ },
+
+ /*
+ * This function is similar to getListSuggestions from
+ * components/satchel/src/nsInputListAutoComplete.js but sadly this one is
+ * used by the autocomplete.xml binding which is not in used in fennec
+ */
+ _getListSuggestions: function formHelperGetListSuggestions(aElement) {
+ if (!(aElement instanceof HTMLInputElement) || !aElement.list)
+ return [];
+
+ let suggestions = [];
+ let filter = !aElement.hasAttribute("mozNoFilter");
+ let lowerFieldValue = aElement.value.toLowerCase();
+
+ let options = aElement.list.options;
+ let length = options.length;
+ for (let i = 0; i < length; i++) {
+ let item = options.item(i);
+
+ let label = item.value;
+ if (item.label)
+ label = item.label;
+ else if (item.text)
+ label = item.text;
+
+ if (filter && label.toLowerCase().indexOf(lowerFieldValue) == -1)
+ continue;
+ suggestions.push({ label: label, value: item.value });
+ }
+
+ return suggestions;
+ },
+
+ _isValidElement: function formHelperIsValidElement(aElement) {
+ if (!aElement.getAttribute)
+ return false;
+
+ let formExceptions = { button: true, checkbox: true, file: true, image: true, radio: true, reset: true, submit: true };
+ if (aElement instanceof HTMLInputElement && formExceptions[aElement.type])
+ return false;
+
+ if (aElement instanceof HTMLButtonElement ||
+ (aElement.getAttribute("role") == "button" && aElement.hasAttribute("tabindex")))
+ return false;
+
+ return this._isNavigableElement(aElement) && this._isVisibleElement(aElement);
+ },
+
+ _isNavigableElement: function formHelperIsNavigableElement(aElement) {
+ if (aElement.disabled || aElement.getAttribute("tabindex") == "-1")
+ return false;
+
+ if (aElement.getAttribute("role") == "button" && aElement.hasAttribute("tabindex"))
+ return true;
+
+ if (this._isSelectElement(aElement) || aElement instanceof HTMLTextAreaElement)
+ return true;
+
+ if (aElement instanceof HTMLInputElement || aElement instanceof HTMLButtonElement)
+ return !(aElement.type == "hidden");
+
+ return this._isEditable(aElement);
+ },
+
+ _isVisibleElement: function formHelperIsVisibleElement(aElement) {
+ let style = aElement ? aElement.ownerDocument.defaultView.getComputedStyle(aElement, null) : null;
+ if (!style)
+ return false;
+
+ let isVisible = (style.getPropertyValue("visibility") != "hidden");
+ let isOpaque = (style.getPropertyValue("opacity") != 0);
+
+ let rect = aElement.getBoundingClientRect();
+
+ // Since the only way to show a drop-down menu for a select when the form
+ // assistant is enabled is to return true here, a select is allowed to have
+ // an opacity to 0 in order to let web developpers add a custom design on
+ // top of it. This is less important to use the form assistant for the
+ // other types of fields because even if the form assistant won't fired,
+ // the focus will be in and a VKB will popup if needed
+ return isVisible && (isOpaque || this._isSelectElement(aElement)) && (rect.height != 0 || rect.width != 0);
+ },
+
+ _isSelectElement: function formHelperIsSelectElement(aElement) {
+ return (aElement instanceof HTMLSelectElement || aElement instanceof XULMenuListElement);
+ },
+
+ /** Caret is used to input text for this element. */
+ _getCaretRect: function _formHelperGetCaretRect() {
+ let element = this.currentElement;
+ let focusedElement = gFocusManager.getFocusedElementForWindow(content, true, {});
+ if (element && (element.mozIsTextField && element.mozIsTextField(false) ||
+ element instanceof HTMLTextAreaElement) && focusedElement == element && this._isVisibleElement(element)) {
+ let utils = Util.getWindowUtils(element.ownerDocument.defaultView);
+ let rect = utils.sendQueryContentEvent(utils.QUERY_CARET_RECT, element.selectionEnd, 0, 0, 0);
+ if (rect) {
+ let scroll = ContentScroll.getScrollOffset(element.ownerDocument.defaultView);
+ return new Rect(scroll.x + rect.left, scroll.y + rect.top, rect.width, rect.height);
+ }
+ }
+
+ return new Rect(0, 0, 0, 0);
+ },
+
+ /** Gets a rect bounding important parts of the element that must be seen when assisting. */
+ _getRect: function _formHelperGetRect() {
+ const kDistanceMax = 100;
+ let element = this.currentElement;
+ let elRect = getBoundingContentRect(element);
+ let labels = this._getLabels();
+ for (let i=0; i b.tabIndex;
+ }
+ this._elements = this._elements.sort(orderByTabIndex);
+
+ // retrieve the correct index
+ let currentIndex = this._getIndexForElement(aElement);
+ return currentIndex;
+ },
+
+ _getIndexForElement: function(aElement) {
+ let currentIndex = -1;
+ let elements = this._elements;
+ for (let i = 0; i < elements.length; i++) {
+ if (elements[i] == aElement)
+ return i;
+ }
+ return -1;
+ },
+
+ _getJSON: function() {
+ let element = this.currentElement;
+ let choices = getListForElement(element);
+ let editable = (element instanceof HTMLInputElement && element.mozIsTextField(false)) || this._isEditable(element);
+
+ let labels = this._getLabels();
+ return {
+ current: {
+ id: element.id,
+ name: element.name,
+ title: labels.length ? labels[0].title : "",
+ value: element.value,
+ maxLength: element.maxLength,
+ type: (element.getAttribute("type") || "").toLowerCase(),
+ choices: choices,
+ isAutocomplete: this._isAutocomplete(element),
+ validationMessage: this.invalidSubmit ? element.validationMessage : null,
+ list: this._getListSuggestions(element),
+ rect: this._getRect(),
+ caretRect: this._getCaretRect(),
+ editable: editable
+ },
+ hasPrevious: !!this._elements[this._currentIndex - 1],
+ hasNext: !!this._elements[this._currentIndex + 1]
+ };
+ },
+
+ /**
+ * For each radio button group, remove all but the checked button
+ * if there is one, or the first button otherwise.
+ */
+ _filterRadioButtons: function(aNodes) {
+ // First pass: Find the checked or first element in each group.
+ let chosenRadios = {};
+ for (let i=0; i < aNodes.length; i++) {
+ let node = aNodes[i];
+ if (node.type == "radio" && (!chosenRadios.hasOwnProperty(node.name) || node.checked))
+ chosenRadios[node.name] = node;
+ }
+
+ // Second pass: Exclude all other radio buttons from the list.
+ let result = [];
+ for (let i=0; i < aNodes.length; i++) {
+ let node = aNodes[i];
+ if (node.type == "radio" && chosenRadios[node.name] != node)
+ continue;
+ result.push(node);
+ }
+ return result;
+ }
+};
+
+
+/******************************************************************************
+ * The next classes wraps some forms elements such as different type of list to
+ * abstract the difference between html and xul element while manipulating them
+ * - SelectWrapper :
+ * - MenulistWrapper :
+ *****************************************************************************/
+
+function getWrapperForElement(aElement) {
+ let wrapper = null;
+ if (aElement instanceof HTMLSelectElement) {
+ wrapper = new SelectWrapper(aElement);
+ }
+ else if (aElement instanceof XULMenuListElement) {
+ wrapper = new MenulistWrapper(aElement);
+ }
+
+ return wrapper;
+}
+
+function getListForElement(aElement) {
+ let wrapper = getWrapperForElement(aElement);
+ if (!wrapper)
+ return null;
+
+ let optionIndex = 0;
+ let result = {
+ multiple: wrapper.getMultiple(),
+ choices: []
+ };
+
+ // Build up a flat JSON array of the choices. In HTML, it's possible for select element choices
+ // to be under a group header (but not recursively). We distinguish between headers and entries
+ // using the boolean "list.group".
+ // XXX If possible, this would be a great candidate for tracing.
+ let children = wrapper.getChildren();
+ for (let i = 0; i < children.length; i++) {
+ let child = children[i];
+ if (wrapper.isGroup(child)) {
+ // This is the group element. Add an entry in the choices that says that the following
+ // elements are a member of this group.
+ result.choices.push({ group: true,
+ text: child.label || child.firstChild.data,
+ disabled: child.disabled
+ });
+ let subchildren = child.children;
+ for (let j = 0; j < subchildren.length; j++) {
+ let subchild = subchildren[j];
+ result.choices.push({
+ group: false,
+ inGroup: true,
+ text: wrapper.getText(subchild),
+ disabled: child.disabled || subchild.disabled,
+ selected: subchild.selected,
+ optionIndex: optionIndex++
+ });
+ }
+ }
+ else if (wrapper.isOption(child)) {
+ // This is a regular choice under no group.
+ result.choices.push({
+ group: false,
+ inGroup: false,
+ text: wrapper.getText(child),
+ disabled: child.disabled,
+ selected: child.selected,
+ optionIndex: optionIndex++
+ });
+ }
+ }
+
+ return result;
+};
+
+
+function SelectWrapper(aControl) {
+ this._control = aControl;
+}
+
+SelectWrapper.prototype = {
+ getSelectedIndex: function() {
+ return this._control.selectedIndex;
+ },
+
+ getMultiple: function() {
+ return this._control.multiple;
+ },
+
+ getOptions: function() {
+ return this._control.options;
+ },
+
+ getChildren: function() {
+ return this._control.children;
+ },
+
+ getText: function(aChild) {
+ return aChild.text;
+ },
+
+ isOption: function(aChild) {
+ return aChild instanceof HTMLOptionElement;
+ },
+
+ isGroup: function(aChild) {
+ return aChild instanceof HTMLOptGroupElement;
+ },
+
+ select: function(aIndex, aSelected) {
+ let options = this._control.options;
+ if (this.getMultiple())
+ options[aIndex].selected = aSelected;
+ else
+ options.selectedIndex = aIndex;
+ },
+
+ fireOnChange: function() {
+ let control = this._control;
+ let evt = this._control.ownerDocument.createEvent("Events");
+ evt.initEvent("change", true, true, this._control.ownerDocument.defaultView, 0,
+ false, false,
+ false, false, null);
+ content.setTimeout(function() {
+ control.dispatchEvent(evt);
+ }, 0);
+ }
+};
+
+
+// bug 559792
+// Use wrappedJSObject when control is in content for extra protection
+function MenulistWrapper(aControl) {
+ this._control = aControl;
+}
+
+MenulistWrapper.prototype = {
+ getSelectedIndex: function() {
+ let control = this._control.wrappedJSObject || this._control;
+ let result = control.selectedIndex;
+ return ((typeof result == "number" && !isNaN(result)) ? result : -1);
+ },
+
+ getMultiple: function() {
+ return false;
+ },
+
+ getOptions: function() {
+ let control = this._control.wrappedJSObject || this._control;
+ return control.menupopup.children;
+ },
+
+ getChildren: function() {
+ let control = this._control.wrappedJSObject || this._control;
+ return control.menupopup.children;
+ },
+
+ getText: function(aChild) {
+ return aChild.label;
+ },
+
+ isOption: function(aChild) {
+ return aChild instanceof Ci.nsIDOMXULSelectControlItemElement;
+ },
+
+ isGroup: function(aChild) {
+ return false;
+ },
+
+ select: function(aIndex, aSelected) {
+ let control = this._control.wrappedJSObject || this._control;
+ control.selectedIndex = aIndex;
+ },
+
+ fireOnChange: function() {
+ let control = this._control;
+ let evt = document.createEvent("XULCommandEvent");
+ evt.initCommandEvent("command", true, true, window, 0,
+ false, false,
+ false, false, null);
+ content.setTimeout(function() {
+ control.dispatchEvent(evt);
+ }, 0);
+ }
+};
diff --git a/browser/metro/base/content/contenthandlers/PluginCTPHandler.js b/browser/metro/base/content/contenthandlers/PluginCTPHandler.js
new file mode 100644
index 000000000000..e5b511b5370c
--- /dev/null
+++ b/browser/metro/base/content/contenthandlers/PluginCTPHandler.js
@@ -0,0 +1,52 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var PluginCTPHandler = {
+ init: function() {
+ addEventListener("PluginClickToPlay", this, false);
+ },
+
+ addLinkClickCallback: function (linkNode, callbackName /*callbackArgs...*/) {
+ // XXX just doing (callback)(arg) was giving a same-origin error. bug?
+ let self = this;
+ let callbackArgs = Array.prototype.slice.call(arguments).slice(2);
+ linkNode.addEventListener("click",
+ function(evt) {
+ if (!evt.isTrusted)
+ return;
+ evt.preventDefault();
+ if (callbackArgs.length == 0)
+ callbackArgs = [ evt ];
+ (self[callbackName]).apply(self, callbackArgs);
+ },
+ true);
+
+ linkNode.addEventListener("keydown",
+ function(evt) {
+ if (!evt.isTrusted)
+ return;
+ if (evt.keyCode == evt.DOM_VK_RETURN) {
+ evt.preventDefault();
+ if (callbackArgs.length == 0)
+ callbackArgs = [ evt ];
+ evt.preventDefault();
+ (self[callbackName]).apply(self, callbackArgs);
+ }
+ },
+ true);
+ },
+
+ handleEvent : function(event) {
+ if (event.type != "PluginClickToPlay")
+ return;
+ let plugin = event.target;
+ PluginHandler.addLinkClickCallback(plugin, "reloadToEnablePlugin");
+ },
+
+ reloadToEnablePlugin: function() {
+ sendAsyncMessage("Browser:PluginClickToPlayClicked", { });
+ }
+};
+
+//PluginCTPHandler.init();
diff --git a/browser/metro/base/content/contenthandlers/SelectionHandler.js b/browser/metro/base/content/contenthandlers/SelectionHandler.js
new file mode 100644
index 000000000000..1d2f29aa603e
--- /dev/null
+++ b/browser/metro/base/content/contenthandlers/SelectionHandler.js
@@ -0,0 +1,1185 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+dump("### SelectionHandler.js loaded\n");
+
+/*
+ http://mxr.mozilla.org/mozilla-central/source/docshell/base/nsIDocShell.idl
+ http://mxr.mozilla.org/mozilla-central/source/content/base/public/nsISelectionDisplay.idl
+ http://mxr.mozilla.org/mozilla-central/source/content/base/public/nsISelectionListener.idl
+ http://mxr.mozilla.org/mozilla-central/source/content/base/public/nsISelectionPrivate.idl
+ http://mxr.mozilla.org/mozilla-central/source/content/base/public/nsISelectionController.idl
+ http://mxr.mozilla.org/mozilla-central/source/content/base/public/nsISelection.idl
+ rangeCount
+ getRangeAt
+ containsNode
+ http://www.w3.org/TR/DOM-Level-2-Traversal-Range/ranges.html
+ http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/range/nsIDOMRange.idl
+ http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/core/nsIDOMDocument.idl#80
+ content.document.createRange()
+ getBoundingClientRect
+ isPointInRange
+ http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/core/nsIDOMNode.idl
+ http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/base/nsIDOMWindowUtils.idl
+ setSelectionAtPoint
+ http://mxr.mozilla.org/mozilla-central/source/dom/interfaces/core/nsIDOMElement.idl
+ getClientRect
+ http://mxr.mozilla.org/mozilla-central/source/layout/generic/nsFrameSelection.h
+ http://mxr.mozilla.org/mozilla-central/source/editor/idl/nsIEditor.idl
+
+ nsIDOMCaretPosition - not implemented
+
+ TODO:
+ - window resize
+ - typing with selection in text input
+ - magnetic monocles should snap to sentence start/end
+ - sub frames:
+ 1) general testing
+ 2) sub frames scroll
+
+*/
+
+var SelectionHandler = {
+ _debugEvents: false,
+ _cache: {},
+ _targetElement: null,
+ _targetIsEditable: false,
+ _contentWindow: null,
+ _contentOffset: { x:0, y:0 },
+ _frameOffset: { x:0, y:0 },
+ _domWinUtils: null,
+ _selectionMoveActive: false,
+ _lastMarker: "",
+ _debugOptions: { dumpRanges: false, displayRanges: false },
+
+ init: function init() {
+ addMessageListener("Browser:SelectionStart", this);
+ addMessageListener("Browser:SelectionEnd", this);
+ addMessageListener("Browser:SelectionMoveStart", this);
+ addMessageListener("Browser:SelectionMove", this);
+ addMessageListener("Browser:SelectionMoveEnd", this);
+ addMessageListener("Browser:SelectionUpdate", this);
+ addMessageListener("Browser:SelectionClose", this);
+ addMessageListener("Browser:SelectionClear", this);
+ addMessageListener("Browser:SelectionCopy", this);
+ addMessageListener("Browser:SelectionDebug", this);
+ },
+
+ shutdown: function shutdown() {
+ removeMessageListener("Browser:SelectionStart", this);
+ removeMessageListener("Browser:SelectionEnd", this);
+ removeMessageListener("Browser:SelectionMoveStart", this);
+ removeMessageListener("Browser:SelectionMove", this);
+ removeMessageListener("Browser:SelectionMoveEnd", this);
+ removeMessageListener("Browser:SelectionUpdate", this);
+ removeMessageListener("Browser:SelectionClose", this);
+ removeMessageListener("Browser:SelectionClear", this);
+ removeMessageListener("Browser:SelectionCopy", this);
+ removeMessageListener("Browser:SelectionDebug", this);
+ },
+
+ isActive: function isActive() {
+ return (this._contentWindow != null);
+ },
+
+ /*************************************************
+ * Browser event handlers
+ */
+
+ /*
+ * Selection start event handler
+ */
+ _onSelectionStart: function _onSelectionStart(aX, aY) {
+ // Init content window information
+ if (!this._initTargetInfo(aX, aY)) {
+ this._onFail("failed to get frame offset");
+ return;
+ }
+
+ // Clear any existing selection from the document
+ let selection = this._contentWindow.getSelection();
+ selection.removeAllRanges();
+
+ Util.dumpLn(this._targetElement);
+
+ // Set our initial selection, aX and aY should be in client coordinates.
+ if (!this._domWinUtils.selectAtPoint(aX, aY, Ci.nsIDOMWindowUtils
+ .SELECT_WORDNOSPACE)) {
+ this._onFail("failed to set selection at point");
+ return;
+ }
+
+ // Update the position of our selection monocles
+ this._updateSelectionUI(true, true);
+ },
+
+ /*
+ * Selection monocle start move event handler
+ */
+ _onSelectionMoveStart: function _onSelectionMoveStart(aMsg) {
+ if (!this._contentWindow) {
+ this._onFail("_onSelectionMoveStart was called without proper view set up");
+ return;
+ }
+
+ if (this._selectionMoveActive) {
+ this._onFail("mouse is already down on drag start?");
+ return;
+ }
+
+ // We bail if things get out of sync here implying we missed a message.
+ this._selectionMoveActive = true;
+
+ // Update the position of our selection monocles
+ this._updateSelectionUI(true, true);
+ },
+
+ /*
+ * Selection monocle move event handler
+ */
+ _onSelectionMove: function _onSelectionMove(aMsg) {
+ if (!this._contentWindow) {
+ this._onFail("_onSelectionMove was called without proper view set up");
+ return;
+ }
+
+ if (!this._selectionMoveActive) {
+ this._onFail("mouse isn't down for drag move?");
+ return;
+ }
+
+ // Update selection in the doc
+ let pos = null;
+ if (aMsg.change == "start") {
+ pos = aMsg.start;
+ } else {
+ pos = aMsg.end;
+ }
+
+ this._handleSelectionPoint(aMsg.change, pos);
+ },
+
+ /*
+ * Selection monocle move finished event handler
+ */
+ _onSelectionMoveEnd: function _onSelectionMoveComplete(aMsg) {
+ if (!this._contentWindow) {
+ this._onFail("_onSelectionMove was called without proper view set up");
+ return;
+ }
+
+ if (!this._selectionMoveActive) {
+ this._onFail("mouse isn't down for drag move?");
+ return;
+ }
+
+ // Update selection in the doc
+ let pos = null;
+ if (aMsg.change == "start") {
+ pos = aMsg.start;
+ } else {
+ pos = aMsg.end;
+ }
+
+ this._handleSelectionPoint(aMsg.change, pos);
+ this._selectionMoveActive = false;
+
+ // _handleSelectionPoint may set a scroll timer, so this must
+ // be reset after the last call.
+ this.clearTimers();
+
+ // Update the position of our selection monocles
+ this._updateSelectionUI(true, true);
+ },
+
+ /*
+ * Selection copy event handler
+ *
+ * Check to see if the incoming click was on our selection rect.
+ * if it was, copy to the clipboard. Incoming coordinates are
+ * content values.
+ */
+ _onSelectionCopy: function _onSelectionCopy(aMsg) {
+ let tap = {
+ xPos: aMsg.xPos, // + this._contentOffset.x,
+ yPos: aMsg.yPos, // + this._contentOffset.y,
+ };
+
+ let tapInSelection = (tap.xPos > this._cache.rect.left &&
+ tap.xPos < this._cache.rect.right) &&
+ (tap.yPos > this._cache.rect.top &&
+ tap.yPos < this._cache.rect.bottom);
+ // Util.dumpLn(tap.xPos, tap.yPos, "|", this._cache.rect.left,
+ // this._cache.rect.right, this._cache.rect.top,
+ // this._cache.rect.bottom);
+ let success = false;
+ let selectedText = this._getSelectedText();
+ if (tapInSelection && selectedText.length) {
+ let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"]
+ .getService(Ci.nsIClipboardHelper);
+ clipboard.copyString(selectedText, this._contentWindow.document);
+ success = true;
+ }
+ sendSyncMessage("Content:SelectionCopied", { succeeded: success });
+ },
+
+ /*
+ * Selection close event handler
+ */
+ _onSelectionClose: function _onSelectionClose() {
+ this._closeSelection();
+ },
+
+ /*
+ * Selection clear event handler
+ */
+ _onSelectionClear: function _onSelectionClear() {
+ this._clearSelection();
+ },
+
+ /*
+ * Called any time SelectionHelperUI would like us to
+ * recalculate the selection bounds.
+ */
+ _onSelectionUpdate: function _onSelectionUpdate() {
+ if (!this._contentWindow) {
+ this._onFail("_onSelectionUpdate was called without proper view set up");
+ return;
+ }
+
+ // Update the position of our selection monocles
+ this._updateSelectionUI(true, true);
+ },
+
+ /*
+ * Called if for any reason we fail during the selection
+ * process. Cancels the selection.
+ */
+ _onFail: function _onFail(aDbgMessage) {
+ if (aDbgMessage && aDbgMessage.length > 0)
+ Util.dumpLn(aDbgMessage);
+ sendAsyncMessage("Content:SelectionFail");
+ this._clearSelection();
+ this._closeSelection();
+ },
+
+ /*
+ * Turning on or off various debug featues.
+ */
+ _onSelectionDebug: function _onSelectionDebug(aMsg) {
+ this._debugOptions = aMsg;
+ this._debugEvents = aMsg.dumpEvents;
+ },
+
+ /*************************************************
+ * Selection helpers
+ */
+
+ /*
+ * _clearSelection
+ *
+ * Clear existing selection if it exists and reset our internla state.
+ */
+ _clearSelection: function _clearSelection() {
+ this.clearTimers();
+ if (this._contentWindow) {
+ let selection = this._getSelection();
+ if (selection)
+ selection.removeAllRanges();
+ } else {
+ let selection = content.getSelection();
+ if (selection)
+ selection.removeAllRanges();
+ }
+ this.selectedText = "";
+ },
+
+ /*
+ * _closeSelection
+ *
+ * Shuts SelectionHandler down.
+ */
+ _closeSelection: function _closeSelection() {
+ this.clearTimers();
+ this._cache = null;
+ this._contentWindow = null;
+ this.selectedText = "";
+ this._selectionMoveActive = false;
+ },
+
+ /*
+ * Informs SelectionHelperUI of the current selection start and end position
+ * so that our selection monocles can be positioned properly.
+ */
+ _updateSelectionUI: function _updateSelectionUI(aUpdateStart, aUpdateEnd) {
+ let selection = this._getSelection();
+
+ // If the range didn't have any text, let's bail
+ if (!selection.toString().trim().length) {
+ this._onFail("no text was present in the current selection");
+ return;
+ }
+
+ // Updates this._cache content selection position data which we send over
+ // to SelectionHelperUI.
+ this._updateUIMarkerRects(selection);
+
+ this._cache.updateStart = aUpdateStart;
+ this._cache.updateEnd = aUpdateEnd;
+
+ // Get monocles positioned correctly
+ sendAsyncMessage("Content:SelectionRange", this._cache);
+ },
+
+ /*
+ * Find content within frames - cache the target nsIDOMWindow,
+ * client coordinate offset, target element, and dom utils interface.
+ */
+ _initTargetInfo: function _initTargetInfo(aX, aY) {
+ // getCurrentWindowAndOffset takes client coordinates
+ let { element: element,
+ contentWindow: contentWindow,
+ offset: offset,
+ frameOffset: frameOffset,
+ utils: utils } =
+ this.getCurrentWindowAndOffset(aX, aY);
+ if (!contentWindow) {
+ return false;
+ }
+ this._targetElement = element;
+ this._contentWindow = contentWindow;
+ this._contentOffset = offset;
+ this._frameOffset = frameOffset;
+ this._domWinUtils = utils;
+ this._targetIsEditable = false;
+ if (this._isTextInput(this._targetElement)) {
+ this._targetIsEditable = true;
+ // Since we have an overlay, focus will not get set, so set it. There
+ // are ways around this if this causes trouble - we have the selection
+ // controller, so we can turn selection display on manually. (Selection
+ // display is setup on edits when focus changes.) I think web pages will
+ // prefer that focus be set when we are interacting with selection in
+ // the element.
+ this._targetElement.focus();
+ }
+ return true;
+ },
+
+ /*
+ * _updateUIMarkerRects(aSelection)
+ *
+ * Extracts the rects of the current selection, clips them to any text
+ * input bounds, and stores them in the cache table we send over to
+ * SelectionHelperUI.
+ */
+ _updateUIMarkerRects: function _updateUIMarkerRects(aSelection) {
+ // Extract the information we'll send over to the ui - cache holds content
+ // coordinate oriented start and end position data. Note the coordinates
+ // of the range passed in are relative the sub frame the range sits in.
+ // SelectionHelperUI calls transformBrowserToClient to get client coords.
+ this._cache = this._extractContentRectFromRange(aSelection.getRangeAt(0),
+ this._contentOffset);
+ if (this. _debugOptions.dumpRanges) {
+ Util.dumpLn("start:", "(" + this._cache.start.xPos + "," +
+ this._cache.start.yPos + ")");
+ Util.dumpLn("end:", "(" + this._cache.end.xPos + "," +
+ this._cache.end.yPos + ")");
+ }
+ this._restrictSelectionRectToEditBounds();
+ },
+
+ /*
+ * Selection bounds will fall outside the bound of a control if the control
+ * can scroll. Clip UI cache data to the bounds of the target so monocles
+ * don't draw outside the control.
+ */
+ _restrictSelectionRectToEditBounds: function _restrictSelectionRectToEditBounds() {
+ if (!this._targetIsEditable)
+ return;
+ let bounds = this._getTargetContentRect();
+ if (this._cache.start.xPos < bounds.left)
+ this._cache.start.xPos = bounds.left;
+ if (this._cache.end.xPos < bounds.left)
+ this._cache.end.xPos = bounds.left;
+ if (this._cache.start.xPos > bounds.right)
+ this._cache.start.xPos = bounds.right;
+ if (this._cache.end.xPos > bounds.right)
+ this._cache.end.xPos = bounds.right;
+
+ if (this._cache.start.yPos < bounds.top)
+ this._cache.start.yPos = bounds.top;
+ if (this._cache.end.yPos < bounds.top)
+ this._cache.end.yPos = bounds.top;
+ if (this._cache.start.yPos > bounds.bottom)
+ this._cache.start.yPos = bounds.bottom;
+ if (this._cache.end.yPos > bounds.bottom)
+ this._cache.end.yPos = bounds.bottom;
+ },
+
+ /*
+ * _handleSelectionPoint(aMarker, aPoint)
+ *
+ * After a monocle moves to a new point in the document, determintes
+ * what the target is and acts on its selection accordingly. If the
+ * monocle is within the bounds of the target, adds or subtracts selection
+ * at the monocle coordinates appropriately and then merges selection ranges
+ * into a single continuous selection. If the monocle is outside the bounds
+ * of the target and the underlying target is editable, uses the selection
+ * controller to advance selection and visibility within the control.
+ */
+ _handleSelectionPoint: function _handleSelectionPoint(aMarker, aClientPoint) {
+ let selection = this._getSelection();
+
+ let clientPoint = { xPos: aClientPoint.xPos, yPos: aClientPoint.yPos };
+
+ if (selection.rangeCount == 0 || selection.rangeCount > 1) {
+ Util.dumpLn("warning, unexpected selection state.");
+ this._setContinuousSelection();
+ return;
+ }
+
+ // Adjust our y position up such that we are sending coordinates on
+ // the text line vs. below it where the monocle is positioned. This
+ // applies to free floating text areas. For text inputs we'll constrain
+ // coordinates further below.
+ let halfLineHeight = this._queryHalfLineHeight(aMarker, selection);
+ clientPoint.yPos -= halfLineHeight;
+
+ if (this._targetIsEditable) {
+ // Check to see if we are beyond the bounds of selection in a input
+ // control. If we are we want to add selection and scroll the added
+ // selection into view.
+ let result = this.updateTextEditSelection(clientPoint);
+
+ // If we're targeting a text input of any kind, make sure clientPoint
+ // is contained within the bounds of the text control. For example, if
+ // a user drags up too close to an upper bounds, selectAtPoint might
+ // select the content above the control. This looks crappy and breaks
+ // our selection rect management.
+ clientPoint =
+ this._constrainPointWithinControl(clientPoint, halfLineHeight);
+
+ // If result.trigger is true, the monocle is outside the bounds of the
+ // control. If it's false, fall through to our additive text selection
+ // below.
+ if (result.trigger) {
+ // _handleSelectionPoint is triggered by input movement, so if we've
+ // tested positive for out-of-bounds scrolling here, we need to set a
+ // recurring timer to keep the expected selection behavior going as
+ // long as the user keeps the monocle out of bounds.
+ if (!this._scrollTimer)
+ this._scrollTimer = new Util.Timeout();
+ this._setTextEditUpdateInterval(result.speed);
+
+ // Smooth the selection
+ this._setContinuousSelection();
+
+ // Update the other monocle's position if we've dragged off to one side
+ if (result.start)
+ this._updateSelectionUI(true, false);
+ if (result.end)
+ this._updateSelectionUI(false, true);
+
+ return;
+ }
+ }
+
+ this._lastMarker = aMarker;
+
+ // If we aren't out-of-bounds, clear the scroll timer if it exists.
+ this.clearTimers();
+
+ // Adjusts the selection based on monocle movement
+ this._adjustSelection(aMarker, clientPoint);
+
+ // Update the other monocle's position. We do this because the dragging
+ // monocle may reset the static monocle to a new position if the dragging
+ // monocle drags ahead or behind the other.
+ if (aMarker == "start") {
+ this._updateSelectionUI(false, true);
+ } else {
+ this._updateSelectionUI(true, false);
+ }
+ },
+
+ /*
+ * _handleSelectionPoint helper methods
+ */
+
+ /*
+ * Based on a monocle marker and position, adds or subtracts from the
+ * existing selection.
+ *
+ * @param the marker currently being manipulated
+ * @param aClientPoint the point designating the new start or end
+ * position for the selection.
+ */
+ _adjustSelection: function _adjustSelection(aMarker, aClientPoint) {
+ // Make a copy of the existing range, we may need to reset it.
+ this._backupRangeList();
+
+ // shrinkSelectionFromPoint takes sub-frame relative coordinates.
+ let framePoint = this._clientPointToFramePoint(aClientPoint);
+
+ // Tests to see if the user is trying to shrink the selection, and if so
+ // collapses it down to the appropriate side such that our calls below
+ // will reset the selection to the proper range.
+ this._shrinkSelectionFromPoint(aMarker, framePoint);
+
+ let selectResult = false;
+ try {
+ // Select a character at the point.
+ selectResult =
+ this._domWinUtils.selectAtPoint(aClientPoint.xPos,
+ aClientPoint.yPos,
+ Ci.nsIDOMWindowUtils.SELECT_CHARACTER);
+ } catch (ex) {
+ }
+
+ // If selectAtPoint failed (which can happen if there's nothing to select)
+ // reset our range back before we shrunk it.
+ if (!selectResult) {
+ this._restoreRangeList();
+ }
+
+ this._freeRangeList();
+
+ // Smooth over the selection between all existing ranges.
+ this._setContinuousSelection();
+ },
+
+ /*
+ * _backupRangeList, _restoreRangeList, and _freeRangeList
+ *
+ * Utilities that manage a cloned copy of the existing selection.
+ */
+
+ _backupRangeList: function _backupRangeList() {
+ this._rangeBackup = new Array();
+ for (let idx = 0; idx < this._getSelection().rangeCount; idx++) {
+ this._rangeBackup.push(this._getSelection().getRangeAt(idx).cloneRange());
+ }
+ },
+
+ _restoreRangeList: function _restoreRangeList() {
+ if (this._rangeBackup == null)
+ return;
+ for (let idx = 0; idx < this._rangeBackup.length; idx++) {
+ this._getSelection().addRange(this._rangeBackup[idx]);
+ }
+ this._freeRangeList();
+ },
+
+ _freeRangeList: function _restoreRangeList() {
+ this._rangeBackup = null;
+ },
+
+ /*
+ * Constrains a selection point within a text input control bounds.
+ *
+ * @param aPoint - client coordinate point
+ * @param aHalfLineHeight - half the line height at the point
+ * @return new constrained point struct
+ */
+ _constrainPointWithinControl: function _cpwc(aPoint, aHalfLineHeight) {
+ let bounds = this._getTargetClientRect();
+ let point = { xPos: aPoint.xPos, yPos: aPoint.yPos };
+ if (point.xPos <= bounds.left)
+ point.xPos = bounds.left + 2;
+ if (point.xPos >= bounds.right)
+ point.xPos = bounds.right - 2;
+ if (point.yPos <= (bounds.top + aHalfLineHeight))
+ point.yPos = (bounds.top + aHalfLineHeight);
+ if (point.yPos >= (bounds.bottom - aHalfLineHeight))
+ point.yPos = (bounds.bottom - aHalfLineHeight);
+ return point;
+ },
+
+ /*
+ * _pointOrientationToRect(aPoint, aRect)
+ *
+ * Returns a table representing which sides of target aPoint is offset
+ * from: { left: offset, top: offset, right: offset, bottom: offset }
+ * Works on client coordinates.
+ */
+ _pointOrientationToRect: function _pointOrientationToRect(aPoint) {
+ let bounds = this._targetElement.getBoundingClientRect();
+ let result = { left: 0, right: 0, top: 0, bottom: 0 };
+ if (aPoint.xPos <= bounds.left)
+ result.left = bounds.left - aPoint.xPos;
+ if (aPoint.xPos >= bounds.right)
+ result.right = aPoint.xPos - bounds.right;
+ if (aPoint.yPos <= bounds.top)
+ result.top = bounds.top - aPoint.yPos;
+ if (aPoint.yPos >= bounds.bottom)
+ result.bottom = aPoint.yPos - bounds.bottom;
+ return result;
+ },
+
+ /*
+ * updateTextEditSelection(aPoint, aClientPoint)
+ *
+ * Checks to see if the monocle point is outside the bounds of the
+ * target edit. If so, use the selection controller to select and
+ * scroll the edit appropriately.
+ *
+ * @param aClientPoint raw pointer position
+ * @return { speed: 0.0 -> 1.0,
+ * trigger: true/false if out of bounds,
+ * start: true/false if updated position,
+ * end: true/false if updated position }
+ */
+ updateTextEditSelection: function updateTextEditSelection(aClientPoint) {
+ if (aClientPoint == undefined) {
+ aClientPoint = this._rawSelectionPoint;
+ }
+ this._rawSelectionPoint = aClientPoint;
+
+ let orientation = this._pointOrientationToRect(aClientPoint);
+ let result = { speed: 1, trigger: false, start: false, end: false };
+
+ if (orientation.left || orientation.top) {
+ this._addEditStartSelection();
+ result.speed = orientation.left + orientation.top;
+ result.trigger = true;
+ result.end = true;
+ } else if (orientation.right || orientation.bottom) {
+ this._addEditEndSelection();
+ result.speed = orientation.right + orientation.bottom;
+ result.trigger = true;
+ result.start = true;
+ }
+
+ // 'speed' is just total pixels offset, so clamp it to something
+ // reasonable callers can work with.
+ if (result.speed > 100)
+ result.speed = 100;
+ if (result.speed < 1)
+ result.speed = 1;
+ result.speed /= 100;
+ return result;
+ },
+
+ _setTextEditUpdateInterval: function _setTextEditUpdateInterval(aSpeedValue) {
+ let timeout = (75 - (aSpeedValue * 75));
+ this._scrollTimer.interval(timeout, this.scrollTimerCallback);
+ },
+
+ /*
+ * Selection control call wrapper
+ */
+ _addEditStartSelection: function _addEditStartSelection() {
+ let selCtrl = this._getSelectController();
+ let selection = this._getSelection();
+ try {
+ this._backupRangeList();
+ selection.collapseToStart();
+ // State: focus = anchor
+ // Only step back if we can, otherwise selCtrl will exception:
+ if (selection.getRangeAt(0).startOffset > 0) {
+ selCtrl.characterMove(false, true);
+ }
+ // State: focus = (anchor - 1)
+ selection.collapseToStart();
+ // State: focus = anchor and both are -1 from the original offset
+ selCtrl.characterMove(true, true);
+ // State: focus = anchor + 1, both have been moved back one char
+ // Restore the rest of the selection:
+ this._restoreRangeList();
+ selCtrl.scrollSelectionIntoView(Ci.nsISelectionController.SELECTION_NORMAL,
+ Ci.nsISelectionController.SELECTION_ANCHOR_REGION,
+ Ci.nsISelectionController.SCROLL_SYNCHRONOUS);
+ } catch (ex) { Util.dumpLn(ex.message);}
+ },
+
+ /*
+ * Selection control call wrapper
+ */
+ _addEditEndSelection: function _addEditEndSelection() {
+ try {
+ let selCtrl = this._getSelectController();
+ selCtrl.characterMove(true, true);
+ selCtrl.scrollSelectionIntoView(Ci.nsISelectionController.SELECTION_NORMAL,
+ Ci.nsISelectionController.SELECTION_FOCUS_REGION,
+ Ci.nsISelectionController.SCROLL_SYNCHRONOUS);
+ } catch (ex) {}
+ },
+
+ /*
+ * _queryHalfLineHeight(aMarker, aSelection)
+ *
+ * Y offset applied to the coordinates of the selection position we send
+ * to dom utils. The selection marker sits below text, but we want the
+ * selection position to be on the text above the monocle. Since text
+ * height can vary across the entire selection range, we need the correct
+ * height based on the line the marker in question is moving on.
+ */
+ _queryHalfLineHeight: function _queryHalfLineHeight(aMarker, aSelection) {
+ let rects = aSelection.getRangeAt(0).getClientRects();
+ if (!rects.length) {
+ return 0;
+ }
+
+ // We are assuming here that these rects are ordered correctly.
+ // From looking at the range code it appears they will be.
+ let height = 0;
+ if (aMarker == "start") {
+ // height of the first rect corresponding to the start marker:
+ height = rects[0].bottom - rects[0].top;
+ } else {
+ // height of the last rect corresponding to the end marker:
+ let len = rects.length - 1;
+ height = rects[len].bottom - rects[len].top;
+ }
+ return height / 2;
+ },
+
+ _findBetterLowerTextRangePoint: function _findBetterLowerTextRangePoint(aClientPoint, aHalfLineHeight) {
+ let range = this._getSelection().getRangeAt(0);
+ let clientRect = range.getBoundingClientRect();
+ if (aClientPoint.y > clientRect.bottom && clientRect.right < aClientPoint.x) {
+ aClientPoint.y = (clientRect.bottom - aHalfLineHeight);
+ this._setDebugPoint(aClientPoint, "red");
+ }
+ },
+
+ /*
+ * _setContinuousSelection()
+ *
+ * Smoothes a selection with multiple ranges into a single
+ * continuous range.
+ */
+ _setContinuousSelection: function _setContinuousSelection() {
+ let selection = this._getSelection();
+ try {
+ if (selection.rangeCount > 1) {
+ let startRange = selection.getRangeAt(0);
+ if (this. _debugOptions.displayRanges) {
+ let clientRect = startRange.getBoundingClientRect();
+ this._setDebugRect(clientRect, "red", false);
+ }
+ let newStartNode = null;
+ let newStartOffset = 0;
+ let newEndNode = null;
+ let newEndOffset = 0;
+ for (let idx = 1; idx < selection.rangeCount; idx++) {
+ let range = selection.getRangeAt(idx);
+ switch (startRange.compareBoundaryPoints(Ci.nsIDOMRange.START_TO_START, range)) {
+ case -1: // startRange is before
+ newStartNode = startRange.startContainer;
+ newStartOffset = startRange.startOffset;
+ break;
+ case 0: // startRange is equal
+ newStartNode = startRange.startContainer;
+ newStartOffset = startRange.startOffset;
+ break;
+ case 1: // startRange is after
+ newStartNode = range.startContainer;
+ newStartOffset = range.startOffset;
+ break;
+ }
+ switch (startRange.compareBoundaryPoints(Ci.nsIDOMRange.END_TO_END, range)) {
+ case -1: // startRange is before
+ newEndNode = range.endContainer;
+ newEndOffset = range.endOffset;
+ break;
+ case 0: // startRange is equal
+ newEndNode = startNode.endContainer;
+ newEndOffset = startNode.endOffset;
+ break;
+ case 1: // startRange is after
+ newEndNode = startRange.endContainer;
+ newEndOffset = startRange.endOffset;
+ break;
+ }
+ if (this. _debugOptions.displayRanges) {
+ let clientRect = range.getBoundingClientRect();
+ this._setDebugRect(clientRect, "orange", false);
+ }
+ }
+ let range = content.document.createRange();
+ range.setStart(newStartNode, newStartOffset);
+ range.setEnd(newEndNode, newEndOffset);
+ selection.addRange(range);
+ }
+ } catch (ex) {
+ Util.dumpLn("exception while modifying selection:", ex.message);
+ this._onFail("_handleSelectionPoint failed.");
+ return false;
+ }
+ return true;
+ },
+
+ /*
+ * _shrinkSelectionFromPoint(aMarker, aFramePoint)
+ *
+ * Tests to see if aFramePoint intersects the current selection and if so,
+ * collapses selection down to the opposite start or end point leaving a
+ * character of selection at the collapse point.
+ *
+ * @param aMarker the marker that is being relocated. ("start" or "end")
+ * @param aFramePoint position of the marker. Should be relative to the
+ * inner frame so that it matches selection range coordinates.
+ */
+ _shrinkSelectionFromPoint: function _shrinkSelectionFromPoint(aMarker, aFramePoint) {
+ try {
+ let selection = this._getSelection();
+ let rects = selection.getRangeAt(0).getClientRects();
+ for (let idx = 0; idx < rects.length; idx++) {
+ if (Util.pointWithinDOMRect(aFramePoint.xPos, aFramePoint.yPos, rects[idx])) {
+ if (aMarker == "start") {
+ selection.collapseToEnd();
+ } else {
+ selection.collapseToStart();
+ }
+ // collapseToStart and collapseToEnd leave an empty range in the
+ // selection at the collapse point. Therefore we need to add some
+ // selection such that the selection added by selectAtPoint and
+ // the current empty range will get merged properly when we smooth
+ // the selection rnages out.
+ let selCtrl = this._getSelectController();
+ // Expand the collapsed range such that it occupies a little space.
+ if (aMarker == "start") {
+ // State: focus = anchor (collapseToEnd does this)
+ selCtrl.characterMove(false, true);
+ // State: focus = (anchor - 1)
+ selection.collapseToStart();
+ // State: focus = anchor and both are -1 from the original offset
+ selCtrl.characterMove(true, true);
+ // State: focus = anchor + 1, both have been moved back one char
+ } else {
+ selCtrl.characterMove(true, true);
+ }
+ break;
+ }
+ }
+ } catch (ex) {
+ Util.dumpLn("error shrinking selection:", ex.message);
+ }
+ },
+
+ /*
+ * Scroll + selection advancement timer when the monocle is
+ * outside the bounds of an input control.
+ */
+ scrollTimerCallback: function scrollTimerCallback() {
+ let result = SelectionHandler.updateTextEditSelection();
+ // Update monocle position and speed if we've dragged off to one side
+ if (result.trigger) {
+ if (result.start)
+ SelectionHandler._updateSelectionUI(true, false);
+ if (result.end)
+ SelectionHandler._updateSelectionUI(false, true);
+ }
+ },
+
+ clearTimers: function clearTimers() {
+ if (this._scrollTimer) {
+ this._scrollTimer.clear();
+ }
+ },
+
+ /*
+ * Events
+ */
+
+ receiveMessage: function sh_receiveMessage(aMessage) {
+ if (this._debugEvents && aMessage.name != "Browser:SelectionMove") {
+ Util.dumpLn("SelectionHandler:", aMessage.name);
+ }
+ let json = aMessage.json;
+ switch (aMessage.name) {
+ case "Browser:SelectionStart":
+ this._onSelectionStart(json.xPos, json.yPos);
+ break;
+
+ case "Browser:SelectionClose":
+ this._onSelectionClose();
+ break;
+
+ case "Browser:SelectionMoveStart":
+ this._onSelectionMoveStart(json);
+ break;
+
+ case "Browser:SelectionMove":
+ this._onSelectionMove(json);
+ break;
+
+ case "Browser:SelectionMoveEnd":
+ this._onSelectionMoveEnd(json);
+ break;
+
+ case "Browser:SelectionCopy":
+ this._onSelectionCopy(json);
+ break;
+
+ case "Browser:SelectionClear":
+ this._onSelectionClear();
+ break;
+
+ case "Browser:SelectionDebug":
+ this._onSelectionDebug(json);
+ break;
+
+ case "Browser:SelectionUpdate":
+ this._onSelectionUpdate();
+ break;
+ }
+ },
+
+ /*
+ * Utilities
+ */
+
+ /*
+ * Returns data on the position of a selection using the relative
+ * coordinates in a range extracted from any sub frames. If aRange
+ * is in the root frame offset should be zero.
+ */
+ _extractContentRectFromRange: function _extractContentRectFromRange(aRange, aOffset) {
+ let cache = {
+ start: {}, end: {},
+ rect: { left: 0, top: 0, right: 0, bottom: 0 }
+ };
+
+ // When in an iframe, aRange coordinates are relative to the frame origin.
+ let rects = aRange.getClientRects();
+
+ let startSet = false;
+ for (let idx = 0; idx < rects.length; idx++) {
+ if (this. _debugOptions.dumpRanges) Util.dumpDOMRect(idx, rects[idx]);
+ if (!startSet && !Util.isEmptyDOMRect(rects[idx])) {
+ cache.start.xPos = rects[idx].left + aOffset.x;
+ cache.start.yPos = rects[idx].bottom + aOffset.y;
+ startSet = true;
+ if (this. _debugOptions.dumpRanges) Util.dumpLn("start set");
+ }
+ if (!Util.isEmptyDOMRect(rects[idx])) {
+ cache.end.xPos = rects[idx].right + aOffset.x;
+ cache.end.yPos = rects[idx].bottom + aOffset.y;
+ if (this. _debugOptions.dumpRanges) Util.dumpLn("end set");
+ }
+ }
+
+ let r = aRange.getBoundingClientRect();
+ cache.rect.left = r.left + aOffset.x;
+ cache.rect.top = r.top + aOffset.y;
+ cache.rect.right = r.right + aOffset.x;
+ cache.rect.bottom = r.bottom + aOffset.y;
+
+ if (!rects.length) {
+ Util.dumpLn("no rects in selection range. unexpected.");
+ }
+
+ return cache;
+ },
+
+ _getTargetContentRect: function _getTargetContentRect() {
+ let client = this._targetElement.getBoundingClientRect();
+ let rect = {};
+ rect.left = client.left + this._contentOffset.x;
+ rect.top = client.top + this._contentOffset.y;
+ rect.right = client.right + this._contentOffset.x;
+ rect.bottom = client.bottom + this._contentOffset.y;
+
+ return rect;
+ },
+
+ _getTargetClientRect: function _getTargetClientRect() {
+ return this._targetElement.getBoundingClientRect();
+ },
+
+ /*
+ * Translate a top level client point to frame relative client point.
+ */
+ _clientPointToFramePoint: function _clientPointToFramePoint(aClientPoint) {
+ let point = {
+ xPos: aClientPoint.xPos - this._frameOffset.x,
+ yPos: aClientPoint.yPos - this._frameOffset.y
+ };
+ return point;
+ },
+
+ /*
+ * Retrieve the total offset from the window's origin to the sub frame
+ * element including frame and scroll offsets. The resulting offset is
+ * such that:
+ * sub frame coords + offset = root frame position
+ */
+ getCurrentWindowAndOffset: function(x, y) {
+ // elementFromPoint: If the element at the given point belongs to another
+ // document (such as an iframe's subdocument), the element in the calling
+ // document's DOM (e.g. the iframe) is returned.
+ let utils = Util.getWindowUtils(content);
+ let element = utils.elementFromPoint(x, y, true, false);
+
+ let offset = { x:0, y:0 };
+ let frameOffset = { x:0, y:0 };
+ let scrollOffset = ContentScroll.getScrollOffset(content);
+ offset.x += scrollOffset.x;
+ offset.y += scrollOffset.y;
+
+ while (element && (element instanceof HTMLIFrameElement ||
+ element instanceof HTMLFrameElement)) {
+ //Util.dumpLn("found child frame:", element.contentDocument.location);
+
+ // Get the content scroll offset in the child frame
+ scrollOffset = ContentScroll.getScrollOffset(element.contentDocument.defaultView);
+ // get the child frame position in client coordinates
+ let rect = element.getBoundingClientRect();
+
+ // subtract frame offset from our elementFromPoint coordinates
+ x -= rect.left;
+ // subtract frame and scroll offset and from elementFromPoint coordinates
+ y -= rect.top + scrollOffset.y;
+
+ // add frame client offset to our total offset result
+ offset.x += rect.left;
+ // add the frame's y offset + scroll offset to our total offset result
+ offset.y += rect.top + scrollOffset.y;
+
+ // Track the offset to the origin of the sub-frame as well
+ frameOffset.x += rect.left;
+ frameOffset.y += rect.top
+
+ // get the frame's nsIDOMWindowUtils
+ utils = element.contentDocument
+ .defaultView
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+
+ // retrieve the target element in the sub frame at x, y
+ element = utils.elementFromPoint(x, y, true, false);
+ }
+
+ if (!element)
+ return {};
+
+ return {
+ element: element,
+ contentWindow: element.ownerDocument.defaultView,
+ offset: offset,
+ frameOffset: frameOffset,
+ utils: utils
+ };
+ },
+
+ _isTextInput: function _isTextInput(aElement) {
+ return ((aElement instanceof Ci.nsIDOMHTMLInputElement &&
+ aElement.mozIsTextField(false)) ||
+ aElement instanceof Ci.nsIDOMHTMLTextAreaElement);
+ },
+
+ _getDocShell: function _getDocShell(aWindow) {
+ if (aWindow == null)
+ return null;
+ return aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShell);
+ },
+
+ _getSelectedText: function _getSelectedText() {
+ let selection = this._getSelection();
+ return selection.toString();
+ },
+
+ _getSelection: function _getSelection() {
+ if (this._targetElement instanceof Ci.nsIDOMNSEditableElement)
+ return this._targetElement
+ .QueryInterface(Ci.nsIDOMNSEditableElement)
+ .editor.selection;
+ else
+ return this._contentWindow.getSelection();
+ },
+
+ _getSelectController: function _getSelectController() {
+ if (this._targetElement instanceof Ci.nsIDOMNSEditableElement) {
+ return this._targetElement
+ .QueryInterface(Ci.nsIDOMNSEditableElement)
+ .editor.selectionController;
+ } else {
+ let docShell = this._getDocShell(this._contentWindow);
+ if (docShell == null)
+ return null;
+ return docShell.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsISelectionDisplay)
+ .QueryInterface(Ci.nsISelectionController);
+ }
+ },
+
+ /*
+ * Debug routines
+ */
+
+ _debugDumpSelection: function _debugDumpSelection(aNote, aSel) {
+ Util.dumpLn("--" + aNote + "--");
+ Util.dumpLn("anchor:", aSel.anchorNode, aSel.anchorOffset);
+ Util.dumpLn("focus:", aSel.focusNode, aSel.focusOffset);
+ },
+
+ _debugDumpChildNodes: function _dumpChildNodes(aNode, aSpacing) {
+ for (let idx = 0; idx < aNode.childNodes.length; idx++) {
+ let node = aNode.childNodes.item(idx);
+ for (let spaceIdx = 0; spaceIdx < aSpacing; spaceIdx++) dump(" ");
+ Util.dumpLn("[" + idx + "]", node);
+ this._debugDumpChildNodes(node, aSpacing + 1);
+ }
+ },
+
+ _setDebugElementRect: function _setDebugElementRect(e, aScrollOffset, aColor) {
+ try {
+ if (e == null) {
+ Util.dumpLn("SelectionHandler _setDebugElementRect(): passed in null element");
+ return;
+ }
+ if (e.offsetWidth == 0 || e.offsetHeight== 0) {
+ Util.dumpLn("SelectionHandler _setDebugElementRect(): passed in flat rect");
+ return;
+ }
+ // e.offset values are positioned relative to the view.
+ sendAsyncMessage("Content:SelectionDebugRect",
+ { left:e.offsetLeft - aScrollOffset.x,
+ top:e.offsetTop - aScrollOffset.y,
+ right:e.offsetLeft + e.offsetWidth - aScrollOffset.x,
+ bottom:e.offsetTop + e.offsetHeight - aScrollOffset.y,
+ color:aColor, id: e.id });
+ } catch(ex) {
+ Util.dumpLn("SelectionHandler _setDebugElementRect():", ex.message);
+ }
+ },
+
+ /*
+ * Adds a debug rect to the selection overlay, useful in identifying
+ * locations for points and rects. Params are in client coordinates.
+ *
+ * Example:
+ * let rect = { left: aPoint.xPos - 1, top: aPoint.yPos - 1,
+ * right: aPoint.xPos + 1, bottom: aPoint.yPos + 1 };
+ * this._setDebugRect(rect, "red");
+ *
+ * In SelectionHelperUI, you'll need to turn on displayDebugLayer
+ * in init().
+ */
+ _setDebugRect: function _setDebugRect(aRect, aColor, aFill, aId) {
+ sendAsyncMessage("Content:SelectionDebugRect",
+ { left:aRect.left, top:aRect.top,
+ right:aRect.right, bottom:aRect.bottom,
+ color:aColor, fill: aFill, id: aId });
+ },
+
+ /*
+ * Adds a small debug rect at the point specified. Params are in
+ * client coordinates.
+ *
+ * In SelectionHelperUI, you'll need to turn on displayDebugLayer
+ * in init().
+ */
+ _setDebugPoint: function _setDebugPoint(aX, aY, aColor) {
+ let rect = { left: aX - 2, top: aY - 2,
+ right: aX + 2, bottom: aY + 2 };
+ this._setDebugRect(rect, aColor, true);
+ },
+};
+
+SelectionHandler.init();
\ No newline at end of file
diff --git a/browser/metro/base/content/contenthandlers/ViewportHandler.js b/browser/metro/base/content/contenthandlers/ViewportHandler.js
new file mode 100644
index 000000000000..4cf9a7c79458
--- /dev/null
+++ b/browser/metro/base/content/contenthandlers/ViewportHandler.js
@@ -0,0 +1,136 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+dump("### ViewportHandler.js loaded\n");
+
+// See getViewportMetadata. Blindly copied from Safari
+// documentation for now.
+const kViewportMinScale = 0;
+const kViewportMaxScale = 10;
+const kViewportMinWidth = 200;
+const kViewportMaxWidth = 10000;
+const kViewportMinHeight = 223;
+const kViewportMaxHeight = 10000;
+
+/*
+ * ViewportHandler
+ *
+ * Plucks zoom info out of web page metadata and forwards it to the
+ * browser.
+ */
+
+let ViewportHandler = {
+ init: function init() {
+ addEventListener("DOMWindowCreated", this, false);
+ addEventListener("DOMMetaAdded", this, false);
+ addEventListener("DOMContentLoaded", this, false);
+ addEventListener("pageshow", this, false);
+ },
+
+ handleEvent: function handleEvent(aEvent) {
+ let target = aEvent.originalTarget;
+ let isRootDocument = (target == content.document || target.ownerDocument == content.document);
+ if (!isRootDocument)
+ return;
+
+ switch (aEvent.type) {
+ case "DOMWindowCreated":
+ this.resetMetadata();
+ break;
+
+ case "DOMMetaAdded":
+ if (target.name == "viewport")
+ this.updateMetadata();
+ break;
+
+ case "DOMContentLoaded":
+ case "pageshow":
+ this.updateMetadata();
+ break;
+ }
+ },
+
+ resetMetadata: function resetMetadata() {
+ sendAsyncMessage("Browser:ViewportMetadata", null);
+ },
+
+ updateMetadata: function updateMetadata() {
+ sendAsyncMessage("Browser:ViewportMetadata", this.getViewportMetadata());
+ },
+
+ /**
+ * Returns an object with the page's preferred viewport properties:
+ * defaultZoom (optional float): The initial scale when the page is loaded.
+ * minZoom (optional float): The minimum zoom level.
+ * maxZoom (optional float): The maximum zoom level.
+ * width (optional int): The CSS viewport width in px.
+ * height (optional int): The CSS viewport height in px.
+ * autoSize (boolean): Resize the CSS viewport when the window resizes.
+ * allowZoom (boolean): Let the user zoom in or out.
+ * autoScale (boolean): Adjust the viewport properties to account for display density.
+ */
+ getViewportMetadata: function getViewportMetadata() {
+ let doctype = content.document.doctype;
+ if (doctype && /(WAP|WML|Mobile)/.test(doctype.publicId))
+ return { defaultZoom: 1, autoSize: true, allowZoom: true, autoScale: true };
+
+ let windowUtils = Util.getWindowUtils(content);
+ let handheldFriendly = windowUtils.getDocumentMetadata("HandheldFriendly");
+ if (handheldFriendly == "true")
+ return { defaultZoom: 1, autoSize: true, allowZoom: true, autoScale: true };
+
+ if (content.document instanceof XULDocument)
+ return { defaultZoom: 1, autoSize: true, allowZoom: false, autoScale: false };
+
+ // HACK: Since we can't set the scale in local tabs (bug 597081), we force
+ // them to device-width and scale=1 so they will lay out reasonably.
+ if (Util.isLocalScheme(content.document.documentURI))
+ return { defaultZoom: 1, autoSize: true, allowZoom: false, autoScale: false };
+
+ // HACK: Since we can't set the scale correctly in frameset pages yet (bug 645756), we force
+ // them to device-width and scale=1 so they will lay out reasonably.
+ if (content.frames.length > 0 && (content.document.body instanceof HTMLFrameSetElement))
+ return { defaultZoom: 1, autoSize: true, allowZoom: false, autoScale: false };
+
+ // viewport details found here
+ // http://developer.apple.com/safari/library/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html
+ // http://developer.apple.com/safari/library/documentation/AppleApplications/Reference/SafariWebContent/UsingtheViewport/UsingtheViewport.html
+
+ // Note: These values will be NaN if parseFloat or parseInt doesn't find a number.
+ // Remember that NaN is contagious: Math.max(1, NaN) == Math.min(1, NaN) == NaN.
+ let scale = parseFloat(windowUtils.getDocumentMetadata("viewport-initial-scale"));
+ let minScale = parseFloat(windowUtils.getDocumentMetadata("viewport-minimum-scale"));
+ let maxScale = parseFloat(windowUtils.getDocumentMetadata("viewport-maximum-scale"));
+
+ let widthStr = windowUtils.getDocumentMetadata("viewport-width");
+ let heightStr = windowUtils.getDocumentMetadata("viewport-height");
+ let width = Util.clamp(parseInt(widthStr), kViewportMinWidth, kViewportMaxWidth);
+ let height = Util.clamp(parseInt(heightStr), kViewportMinHeight, kViewportMaxHeight);
+
+ let allowZoomStr = windowUtils.getDocumentMetadata("viewport-user-scalable");
+ let allowZoom = !/^(0|no|false)$/.test(allowZoomStr); // WebKit allows 0, "no", or "false"
+
+ scale = Util.clamp(scale, kViewportMinScale, kViewportMaxScale);
+ minScale = Util.clamp(minScale, kViewportMinScale, kViewportMaxScale);
+ maxScale = Util.clamp(maxScale, kViewportMinScale, kViewportMaxScale);
+
+ // If initial scale is 1.0 and width is not set, assume width=device-width
+ let autoSize = (widthStr == "device-width" ||
+ (!widthStr && (heightStr == "device-height" || scale == 1.0)));
+
+ return {
+ defaultZoom: scale,
+ minZoom: minScale,
+ maxZoom: maxScale,
+ width: width,
+ height: height,
+ autoSize: autoSize,
+ allowZoom: allowZoom,
+ autoScale: true
+ };
+ }
+};
+
+ViewportHandler.init();
+
diff --git a/browser/metro/base/content/cursor.css b/browser/metro/base/content/cursor.css
new file mode 100644
index 000000000000..4f976b2ca152
--- /dev/null
+++ b/browser/metro/base/content/cursor.css
@@ -0,0 +1,14 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@namespace xul url(http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul);
+@namespace html url(http://www.w3.org/1999/xhtml);
+
+xul|*:-moz-system-metric(touch-enabled) {
+ cursor: none !important;
+}
+
+html|*:-moz-system-metric(touch-enabled) {
+ cursor: none !important;
+}
diff --git a/browser/metro/base/content/downloads.js b/browser/metro/base/content/downloads.js
new file mode 100644
index 000000000000..9f85db48dac0
--- /dev/null
+++ b/browser/metro/base/content/downloads.js
@@ -0,0 +1,469 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const URI_GENERIC_ICON_DOWNLOAD = "chrome://browser/skin/images/alert-downloads-30.png";
+
+var Downloads = {
+ _inited: false,
+ _progressAlert: null,
+
+ get manager() {
+ return Cc["@mozilla.org/download-manager;1"]
+ .getService(Ci.nsIDownloadManager);
+ },
+
+ _getReferrerOrSource: function dh__getReferrerOrSource(aDownload) {
+ return aDownload.referrer.spec || aDownload.source.spec;
+ },
+
+ _getLocalFile: function dh__getLocalFile(aFileURI) {
+ // XXX it's possible that using a null char-set here is bad
+
+ const fileUrl = Services.io.newURI(aFileURI.spec || aFileURI, null, null).QueryInterface(Ci.nsIFileURL);
+ return fileUrl.file.clone().QueryInterface(Ci.nsILocalFile);
+ },
+
+ init: function dh_init() {
+ if (this._inited)
+ return;
+
+ this._inited = true;
+
+ Services.obs.addObserver(this, "dl-start", true);
+ Services.obs.addObserver(this, "dl-done", true);
+ },
+
+ uninit: function dh_uninit() {
+ Services.obs.removeObserver(this, "dl-start");
+ Services.obs.removeObserver(this, "dl-done");
+ },
+
+ openDownload: function dh_openDownload(aDownload) {
+ // expects xul item
+ let id = aDownload.getAttribute("downloadId");
+ let download = this.manager.getDownload(id)
+ let fileURI = download.target;
+ let file = this._getLocalFile(fileURI);
+
+ try {
+ file.launch();
+ } catch (ex) { }
+ },
+
+ removeDownload: function dh_removeDownload(aDownload) {
+ // aDownload is the XUL element here,
+ // and .target currently returns the target attribute (string value)
+ let id = aDownload.getAttribute("downloadId");
+ let download = this.manager.getDownload(id);
+
+ if (download) {
+ // TODO add class/mark element for removal transition?
+ this.manager.removeDownload(id);
+ }
+ },
+
+ cancelDownload: function dh_cancelDownload(aDownload) {
+ let id = aDownload.getAttribute("downloadId");
+ let download = this.manager.getDownload(id)
+ this.manager.cancelDownload(id);
+
+ let fileURI = download.target;
+ let file = this._getLocalFile(fileURI);
+
+ if (file.exists())
+ file.remove(false);
+ },
+
+ pauseDownload: function dh_pauseDownload(aDownload) {
+ let id = aDownload.getAttribute("downloadId");
+ this.manager.pauseDownload(id);
+ },
+
+ resumeDownload: function dh_resumeDownload(aDownload) {
+ let id = aDownload.getAttribute("downloadId");
+ this.manager.resumeDownload(id);
+ },
+
+ retryDownload: function dh_retryDownload(aDownload) {
+ let id = aDownload.getAttribute("downloadId");
+ this.manager.retryDownload(id);
+ },
+
+ showPage: function dh_showPage(aDownload) {
+ let id = aDownload.getAttribute("downloadId");
+ let download = this.manager.getDownload(id)
+ let uri = this._getReferrerOrSource(download);
+ if (uri)
+ BrowserUI.newTab(uri, Browser.selectedTab);
+ },
+
+ showAlert: function dh_showAlert(aName, aMessage, aTitle, aIcon) {
+ var notifier = Cc["@mozilla.org/alerts-service;1"]
+ .getService(Ci.nsIAlertsService);
+
+ // Callback for tapping on the alert popup
+ let observer = {
+ observe: function (aSubject, aTopic, aData) {
+ if (aTopic == "alertclickcallback")
+ PanelUI.show("downloads-container");
+ }
+ };
+
+ if (!aTitle)
+ aTitle = Strings.browser.GetStringFromName("alertDownloads");
+
+ if (!aIcon)
+ aIcon = URI_GENERIC_ICON_DOWNLOAD;
+
+ notifier.showAlertNotification(aIcon, aTitle, aMessage, true, "", observer, aName);
+ },
+
+ observe: function (aSubject, aTopic, aData) {
+ let download = aSubject.QueryInterface(Ci.nsIDownload);
+ let msgKey = "";
+
+ switch (aTopic) {
+ case "dl-start":
+ msgKey = "alertDownloadsStart";
+ if (!this._progressAlert) {
+ this._progressAlert = new AlertDownloadProgressListener();
+ this.manager.addListener(this._progressAlert);
+ }
+ break;
+ case "dl-done":
+ msgKey = "alertDownloadsDone";
+ break;
+ }
+
+ if (msgKey) {
+ let message = Strings.browser.formatStringFromName(msgKey, [download.displayName], 1);
+ let url = download.target.spec.replace("file:", "download:");
+ this.showAlert(url, message);
+ }
+ },
+
+ QueryInterface: function (aIID) {
+ if (!aIID.equals(Ci.nsIObserver) &&
+ !aIID.equals(Ci.nsISupportsWeakReference) &&
+ !aIID.equals(Ci.nsISupports))
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ return this;
+ }
+};
+
+/**
+ * Wraps a list/grid control implementing nsIDOMXULSelectControlElement and
+ * fills it with the user's downloads. It observes download notifications,
+ * so it'll automatically update to reflect current download progress.
+ *
+ * @param aSet Control implementing nsIDOMXULSelectControlElement.
+ * @param {Number} aLimit Maximum number of items to show in the view.
+ */
+function DownloadsView(aSet, aLimit) {
+ this._set = aSet;
+ this._limit = aLimit;
+
+ this._progress = new DownloadProgressListener(this);
+ Downloads.manager.addListener(this._progress);
+
+ // Look for the removed download notification
+ let obs = Cc["@mozilla.org/observer-service;1"].
+ getService(Ci.nsIObserverService);
+ obs.addObserver(this, "download-manager-remove-download-guid", false);
+
+ this.getDownloads();
+}
+
+DownloadsView.prototype = {
+ _progress: null,
+ _stmt: null,
+ _timeoutID: null,
+ _set: null,
+ _limit: null,
+
+ _getItemForDownloadGuid: function dv__getItemForDownload(aGuid) {
+ return this._set.querySelector("richgriditem[downloadGuid='" + aGuid + "']");
+ },
+
+ _getItemForDownload: function dv__getItemForDownload(aDownload) {
+ return this._set.querySelector("richgriditem[downloadId='" + aDownload.id + "']");
+ },
+
+ _getDownloadForItem: function dv__getDownloadForItem(anItem) {
+ let id = anItem.getAttribute("downloadId");
+ return Downloads.manager.getDownload(id);
+ },
+
+ _getAttrsForDownload: function dv__getAttrsForDownload(aDownload) {
+ // expects a DownloadManager download object
+ return {
+ typeName: 'download',
+ downloadId: aDownload.id,
+ downloadGuid: aDownload.guid,
+ name: aDownload.displayName,
+ target: aDownload.target,
+ iconURI: "moz-icon://" + aDownload.displayName + "?size=64",
+ date: DownloadUtils.getReadableDates(new Date())[0],
+ domain: DownloadUtils.getURIHost(aDownload.source.spec)[0],
+ size: this._getDownloadSize(aDownload.size),
+ state: aDownload.state
+ };
+
+ },
+ _updateItemWithAttrs: function dv__updateItemWithAttrs(anItem, aAttrs) {
+ for (let name in aAttrs)
+ anItem.setAttribute(name, aAttrs[name]);
+ },
+
+ _getDownloadSize: function dv__getDownloadSize (aSize) {
+ let displaySize = DownloadUtils.convertByteUnits(aSize);
+ if (displaySize[0] > 0) // [0] is size, [1] is units
+ return displaySize.join("");
+ else
+ return Strings.browser.GetStringFromName("downloadsUnknownSize");
+ },
+
+ _initStatement: function dv__initStatement() {
+ if (this._stmt)
+ this._stmt.finalize();
+
+ let limitClause = this._limit ? ("LIMIT " + this._limit) : "";
+
+ this._stmt = Downloads.manager.DBConnection.createStatement(
+ "SELECT id, guid, name, target, source, state, startTime, endTime, referrer, " +
+ "currBytes, maxBytes, state IN (?1, ?2, ?3, ?4, ?5) isActive " +
+ "FROM moz_downloads " +
+ "ORDER BY isActive DESC, endTime DESC, startTime DESC " +
+ limitClause);
+ },
+
+ _stepDownloads: function dv__stepDownloads(aNumItems) {
+ try {
+ if (!this._stmt.executeStep()) {
+ // final record;
+ this._stmt.finalize();
+ this._stmt = null;
+ this._fire("DownloadsReady", this._set);
+ return;
+ }
+ let attrs = {
+ typeName: 'download',
+ // TODO: the remove event gives us guid not id; should we use guid as the download (record) id?
+ downloadGuid: this._stmt.row.guid,
+ downloadId: this._stmt.row.id,
+ name: this._stmt.row.name,
+ target: this._stmt.row.target,
+ iconURI: "moz-icon://" + this._stmt.row.name + "?size=25",
+ date: DownloadUtils.getReadableDates(new Date(this._stmt.row.endTime / 1000))[0],
+ domain: DownloadUtils.getURIHost(this._stmt.row.source)[0],
+ size: this._getDownloadSize(this._stmt.row.maxBytes),
+ state: this._stmt.row.state
+ };
+
+ let item = this._set.appendItem(attrs.target, attrs.downloadId);
+ this._updateItemWithAttrs(item, attrs);
+ } catch (e) {
+ // Something went wrong when stepping or getting values, so quit
+ this._stmt.reset();
+ return;
+ }
+
+ // Add another item to the list if we should;
+ // otherwise, let the UI update and continue later
+ if (aNumItems > 1) {
+ this._stepDownloads(aNumItems - 1);
+ } else {
+ // Use a shorter delay for earlier downloads to display them faster
+ let delay = Math.min(this._set.itemCount * 10, 300);
+ let self = this;
+ this._timeoutID = setTimeout(function() { self._stepDownloads(5); }, delay);
+ }
+ },
+
+ _fire: function _fire(aName, anElement) {
+ let event = document.createEvent("Events");
+ event.initEvent(aName, true, true);
+ anElement.dispatchEvent(event);
+ },
+
+ observe: function dv_managerObserver(aSubject, aTopic, aData) {
+ // observer-service message handler
+ switch (aTopic) {
+ case "download-manager-remove-download-guid":
+ let guid = aSubject.QueryInterface(Ci.nsISupportsCString);
+ this.removeDownload({
+ guid: guid
+ });
+ return;
+ }
+ },
+
+ getDownloads: function dv_getDownloads() {
+ this._initStatement();
+ clearTimeout(this._timeoutID);
+
+ // Since we're pulling in all downloads, clear the list to avoid duplication
+ this.clearDownloads();
+
+ this._stmt.reset();
+ this._stmt.bindInt32Parameter(0, Ci.nsIDownloadManager.DOWNLOAD_NOTSTARTED);
+ this._stmt.bindInt32Parameter(1, Ci.nsIDownloadManager.DOWNLOAD_DOWNLOADING);
+ this._stmt.bindInt32Parameter(2, Ci.nsIDownloadManager.DOWNLOAD_PAUSED);
+ this._stmt.bindInt32Parameter(3, Ci.nsIDownloadManager.DOWNLOAD_QUEUED);
+ this._stmt.bindInt32Parameter(4, Ci.nsIDownloadManager.DOWNLOAD_SCANNING);
+
+ // Take a quick break before we actually start building the list
+ let self = this;
+ this._timeoutID = setTimeout(function() {
+ self._stepDownloads(1);
+ }, 0);
+ },
+
+ clearDownloads: function dv_clearDownloads() {
+ while (this._set.itemCount > 0)
+ this._set.removeItemAt(0);
+ },
+
+ addDownload: function dv_addDownload(aDownload) {
+ // expects a download manager download object
+ let attrs = this._getAttrsForDownload(aDownload);
+ let item = this._set.insertItemAt(0, attrs.target, attrs.downloadId);
+ this._updateItemWithAttrs(item, attrs);
+ },
+
+ updateDownload: function dv_updateDownload(aDownload) {
+ // expects a download manager download object
+ let item = this._getItemForDownload(aDownload);
+
+ if (!item)
+ return;
+
+ let attrs = this._getAttrsForDownload(aDownload);
+ this._updateItemWithAttrs(item, attrs);
+ },
+
+ removeDownload: function dv_removeDownload(aDownload) {
+ // expects an object with id or guid property
+ let item;
+ if (aDownload.id) {
+ item = this._getItemForDownload(aDownload.id);
+ } else if (aDownload.guid) {
+ item = this._getItemForDownloadGuid(aDownload.guid);
+ }
+ if (!item)
+ return;
+
+ let idx = this._set.getIndexOfItem(item);
+ if (idx < 0)
+ return;
+ // any transition needed here?
+ this._set.removeItemAt(idx);
+ },
+
+ destruct: function dv_destruct() {
+ Downloads.manager.removeListener(this._progress);
+ }
+};
+
+var DownloadsPanelView = {
+ _view: null,
+
+ get _grid() { return document.getElementById("downloads-list"); },
+ get visible() { return PanelUI.isPaneVisible("downloads-container"); },
+
+ init: function init() {
+ this._view = new DownloadsView(this._grid);
+ },
+
+ show: function show() {
+ this._grid.arrangeItems();
+ },
+
+ uninit: function uninit() {
+ this._view.destruct();
+ }
+};
+
+/**
+ * Notifies a DownloadsView about updates in the state of various downloads.
+ *
+ * @param aView An instance of DownloadsView.
+ */
+function DownloadProgressListener(aView) {
+ this._view = aView;
+}
+
+DownloadProgressListener.prototype = {
+ _view: null,
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// nsIDownloadProgressListener
+ onDownloadStateChange: function dPL_onDownloadStateChange(aState, aDownload) {
+ let state = aDownload.state;
+ switch (state) {
+ case Ci.nsIDownloadManager.DOWNLOAD_QUEUED:
+ case Ci.nsIDownloadManager.DOWNLOAD_BLOCKED_POLICY:
+ this._view.addDownload(aDownload);
+ break;
+ case Ci.nsIDownloadManager.DOWNLOAD_FAILED:
+ case Ci.nsIDownloadManager.DOWNLOAD_CANCELED:
+ case Ci.nsIDownloadManager.DOWNLOAD_BLOCKED_PARENTAL:
+ case Ci.nsIDownloadManager.DOWNLOAD_DIRTY:
+ case Ci.nsIDownloadManager.DOWNLOAD_FINISHED:
+ break;
+ }
+
+ this._view.updateDownload(aDownload);
+ },
+
+ onProgressChange: function dPL_onProgressChange(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress, aDownload) {
+ // TODO : Add more detailed progress information.
+ this._view.updateDownload(aDownload);
+ },
+
+ onStateChange: function(aWebProgress, aRequest, aState, aStatus, aDownload) { },
+ onSecurityChange: function(aWebProgress, aRequest, aState, aDownload) { },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// nsISupports
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIDownloadProgressListener])
+};
+
+
+/**
+ * Tracks download progress so that additional information can be displayed
+ * about its download in alert popups.
+ */
+function AlertDownloadProgressListener() { }
+
+AlertDownloadProgressListener.prototype = {
+ //////////////////////////////////////////////////////////////////////////////
+ //// nsIDownloadProgressListener
+ onProgressChange: function(aWebProgress, aRequest, aCurSelfProgress, aMaxSelfProgress, aCurTotalProgress, aMaxTotalProgress, aDownload) {
+ let strings = Strings.browser;
+ let availableSpace = -1;
+
+ try {
+ // diskSpaceAvailable is not implemented on all systems
+ let availableSpace = aDownload.targetFile.diskSpaceAvailable;
+ } catch(ex) { }
+
+ let contentLength = aDownload.size;
+ if (availableSpace > 0 && contentLength > 0 && contentLength > availableSpace) {
+ Downloads.showAlert(aDownload.target.spec.replace("file:", "download:"),
+ strings.GetStringFromName("alertDownloadsNoSpace"),
+ strings.GetStringFromName("alertDownloadsSize"));
+ Downloads.cancelDownload(aDownload.id);
+ }
+ },
+
+ onDownloadStateChange: function(aState, aDownload) { },
+ onStateChange: function(aWebProgress, aRequest, aState, aStatus, aDownload) { },
+ onSecurityChange: function(aWebProgress, aRequest, aState, aDownload) { },
+
+ //////////////////////////////////////////////////////////////////////////////
+ //// nsISupports
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIDownloadProgressListener])
+};
diff --git a/browser/metro/base/content/exceptions.js b/browser/metro/base/content/exceptions.js
new file mode 100644
index 000000000000..0866762705b6
--- /dev/null
+++ b/browser/metro/base/content/exceptions.js
@@ -0,0 +1,115 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+let Cc = Components.classes;
+let Ci = Components.interfaces;
+let Cu = Components.utils;
+
+Cu.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+/**
+ A class to add exceptions to override SSL certificate problems. The functionality
+ itself is borrowed from exceptionDialog.js.
+*/
+function SSLExceptions() {
+ this._overrideService = Cc["@mozilla.org/security/certoverride;1"]
+ .getService(Ci.nsICertOverrideService);
+}
+
+
+SSLExceptions.prototype = {
+ _overrideService: null,
+ _sslStatus: null,
+
+ getInterface: function SSLE_getInterface(aIID) {
+ return this.QueryInterface(aIID);
+ },
+ QueryInterface: function SSLE_QueryInterface(aIID) {
+ if (aIID.equals(Ci.nsIBadCertListener2) ||
+ aIID.equals(Ci.nsISupports))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ },
+
+ /**
+ To collect the SSL status we intercept the certificate error here
+ and store the status for later use.
+ */
+ notifyCertProblem: function SSLE_notifyCertProblem(socketInfo, sslStatus, targetHost) {
+ this._sslStatus = sslStatus.QueryInterface(Ci.nsISSLStatus);
+ return true; // suppress error UI
+ },
+
+ /**
+ Attempt to download the certificate for the location specified to get the SSLState
+ for the certificate and the errors.
+ */
+ _checkCert: function SSLE_checkCert(aURI) {
+ this._sslStatus = null;
+
+ var req = new XMLHttpRequest();
+ try {
+ if(aURI) {
+ req.open("GET", aURI.prePath, false);
+ req.channel.notificationCallbacks = this;
+ req.send(null);
+ }
+ } catch (e) {
+ // We *expect* exceptions if there are problems with the certificate
+ // presented by the site. Log it, just in case, but we can proceed here,
+ // with appropriate sanity checks
+ Components.utils.reportError("Attempted to connect to a site with a bad certificate in the add exception dialog. " +
+ "This results in a (mostly harmless) exception being thrown. " +
+ "Logged for information purposes only: " + e);
+ }
+
+ return this._sslStatus;
+ },
+
+ /**
+ Internal method to create an override.
+ */
+ _addOverride: function SSLE_addOverride(aURI, aWindow, temporary) {
+ var SSLStatus = this._checkCert(aURI);
+ var certificate = SSLStatus.serverCert;
+
+ var flags = 0;
+
+ // in private browsing do not store exceptions permanently ever
+ if (PrivateBrowsingUtils.isWindowPrivate(aWindow)) {
+ temporary = true;
+ }
+
+ if(SSLStatus.isUntrusted)
+ flags |= this._overrideService.ERROR_UNTRUSTED;
+ if(SSLStatus.isDomainMismatch)
+ flags |= this._overrideService.ERROR_MISMATCH;
+ if(SSLStatus.isNotValidAtThisTime)
+ flags |= this._overrideService.ERROR_TIME;
+
+ this._overrideService.rememberValidityOverride(
+ aURI.asciiHost,
+ aURI.port,
+ certificate,
+ flags,
+ temporary);
+ },
+
+ /**
+ Creates a permanent exception to override all overridable errors for
+ the given URL.
+ */
+ addPermanentException: function SSLE_addPermanentException(aURI, aWindow) {
+ this._addOverride(aURI, aWindow, false);
+ },
+
+ /**
+ Creates a temporary exception to override all overridable errors for
+ the given URL.
+ */
+ addTemporaryException: function SSLE_addTemporaryException(aURI, aWindow) {
+ this._addOverride(aURI, aWindow, true);
+ }
+};
diff --git a/browser/metro/base/content/helperui/AlertsHelper.js b/browser/metro/base/content/helperui/AlertsHelper.js
new file mode 100644
index 000000000000..9e101b351485
--- /dev/null
+++ b/browser/metro/base/content/helperui/AlertsHelper.js
@@ -0,0 +1,82 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var AlertsHelper = {
+ _timeoutID: -1,
+ _listener: null,
+ _cookie: "",
+ _clickable: false,
+ get container() {
+ delete this.container;
+ let container = document.getElementById("alerts-container");
+
+ let self = this;
+ container.addEventListener("transitionend", function() {
+ self.alertTransitionOver();
+ }, true);
+
+ return this.container = container;
+ },
+
+ showAlertNotification: function ah_show(aImageURL, aTitle, aText, aTextClickable, aCookie, aListener) {
+ this._clickable = aTextClickable || false;
+ this._listener = aListener || null;
+ this._cookie = aCookie || "";
+
+ // Reset the container settings from the last time so layout can happen naturally
+ let container = this.container;
+ container.removeAttribute("width");
+ let alertText = document.getElementById("alerts-text");
+ alertText.style.whiteSpace = "";
+
+ document.getElementById("alerts-image").setAttribute("src", aImageURL);
+ document.getElementById("alerts-title").value = aTitle;
+ alertText.textContent = aText;
+
+ container.hidden = false;
+ let bcr = container.getBoundingClientRect();
+ if (bcr.width > window.innerWidth - 50) {
+ // If the window isn't wide enough, we need to re-layout
+ container.setAttribute("width", window.innerWidth - 50); // force a max width
+ alertText.style.whiteSpace = "pre-wrap"; // wrap text as needed
+ bcr = container.getBoundingClientRect(); // recalculate the bcr
+ }
+ container.setAttribute("width", bcr.width); // redundant but cheap
+ container.setAttribute("height", bcr.height);
+
+ container.classList.add("showing");
+
+ let timeout = Services.prefs.getIntPref("alerts.totalOpenTime");
+ let self = this;
+ if (this._timeoutID)
+ clearTimeout(this._timeoutID);
+ this._timeoutID = setTimeout(function() { self._timeoutAlert(); }, timeout);
+ },
+
+ _timeoutAlert: function ah__timeoutAlert() {
+ this._timeoutID = -1;
+
+ this.container.classList.remove("showing");
+ if (this._listener)
+ this._listener.observe(null, "alertfinished", this._cookie);
+ },
+
+ alertTransitionOver: function ah_alertTransitionOver() {
+ let container = this.container;
+ if (!container.classList.contains("showing")) {
+ container.height = 0;
+ container.hidden = true;
+ }
+ },
+
+ click: function ah_click(aEvent) {
+ if (this._clickable && this._listener)
+ this._listener.observe(null, "alertclickcallback", this._cookie);
+
+ if (this._timeoutID != -1) {
+ clearTimeout(this._timeoutID);
+ this._timeoutAlert();
+ }
+ }
+};
diff --git a/browser/metro/base/content/helperui/CaptureDialog.js b/browser/metro/base/content/helperui/CaptureDialog.js
new file mode 100644
index 000000000000..97cb2e057cd6
--- /dev/null
+++ b/browser/metro/base/content/helperui/CaptureDialog.js
@@ -0,0 +1,145 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var CaptureDialog = {
+ _video: null,
+ _container: null,
+ _lastOrientationEvent: null,
+
+ init: function() {
+ document.getElementsByAttribute('command', 'cmd_ok')[0].focus();
+ this._video = document.getElementById("capturepicker-video");
+ this._container = document.getElementById("capturepicker-container");
+ window.addEventListener("resize", this, false);
+ window.addEventListener("deviceorientation", this, false);
+ this.handleEvent({ type: "resize" });
+ },
+
+ setPreviewOrientation: function(aEvent) {
+ if (window.innerWidth < window.innerHeight) {
+ if (aEvent.beta > 0)
+ this._video.style.MozTransform = "rotate(90deg)";
+ else
+ this._video.style.MozTransform = "rotate(-90deg)";
+ this._container.classList.add("vertical");
+ }
+ else {
+ if (aEvent.gamma > 0)
+ this._video.style.MozTransform = "rotate(180deg)";
+ else
+ this._video.style.MozTransform = "";
+ this._container.classList.remove("vertical");
+ }
+ },
+
+ handleEvent: function(aEvent) {
+ if (aEvent.type == "deviceorientation") {
+ if (!this._lastOrientationEvent)
+ this.setPreviewOrientation(aEvent);
+ this._lastOrientationEvent = aEvent;
+ }
+ else if (aEvent.type == "resize") {
+ if (this._lastOrientationEvent)
+ this.setPreviewOrientation(this._lastOrientationEvent);
+ }
+ },
+
+ cancel: function() {
+ this.doClose(false);
+ },
+
+ capture: function() {
+ this.doClose(true);
+ },
+
+ doClose: function(aResult) {
+ window.removeEventListener("resize", this, false);
+ window.removeEventListener("deviceorientation", this, false);
+ let dialog = document.getElementById("capturepicker-dialog");
+ dialog.arguments.result = aResult;
+ if (aResult)
+ dialog.arguments.path = this.saveFrame();
+ document.getElementById("capturepicker-video").setAttribute("src", "");
+ dialog.close();
+ },
+
+ saveFrame: function() {
+ let Cc = Components.classes;
+ let Ci = Components.interfaces;
+ Components.utils.import("resource://gre/modules/Services.jsm");
+
+ let width = 320;
+ let height = 240;
+ let rotation = 0;
+
+ if (window.innerWidth < window.innerHeight) {
+ width = 240;
+ height = 320;
+ if (this._lastOrientationEvent.beta > 0)
+ rotation = Math.PI / 2;
+ else
+ rotation = -Math.PI / 2;
+ }
+ else {
+ if (this._lastOrientationEvent.gamma > 0)
+ rotation = Math.PI;
+ }
+
+ let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+ canvas.width = width;
+ canvas.height = height;
+ let ctx = canvas.getContext("2d");
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ //ctx.save();
+ if (rotation != 0) {
+ if (rotation == Math.PI / 2)
+ ctx.translate(width, 0);
+ else if (rotation == -Math.PI / 2)
+ ctx.translate(-width, 0);
+ else
+ ctx.translate(width, height);
+ ctx.rotate(rotation);
+ }
+ ctx.drawImage(document.getElementById("capturepicker-video"), 0, 0, 320, 240);
+ //ctx.restore();
+ let url = canvas.toDataURL("image/png");
+
+ let tmpDir = Services.dirsvc.get("TmpD", Ci.nsIFile);
+ let file = tmpDir.clone();
+ file.append("capture.png");
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0600);
+ let persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"].createInstance(Ci.nsIWebBrowserPersist);
+
+ persist.persistFlags = Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES | Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
+
+ let mDone = false;
+ let mFailed = false;
+
+ let progressListener = {
+ onProgressChange: function() {
+ /* Ignore progress callback */
+ },
+ onStateChange: function(aProgress, aRequest, aStateFlag, aStatus) {
+ if (aStateFlag & Ci.nsIWebProgressListener.STATE_STOP) {
+ mDone = true;
+ }
+ }
+ }
+ persist.progressListener = progressListener;
+
+ let source = Services.io.newURI(url, "UTF8", null);;
+ persist.saveURI(source, null, null, null, null, file);
+
+ // don't wait more than 3 seconds for a successful save
+ window.setTimeout(function() {
+ mDone = true;
+ mFailed = true;
+ }, 3000);
+
+ while (!mDone)
+ Services.tm.currentThread.processNextEvent(true);
+
+ return (mFailed ? null : file.path);
+ }
+}
diff --git a/browser/metro/base/content/helperui/CapturePickerUI.js b/browser/metro/base/content/helperui/CapturePickerUI.js
new file mode 100644
index 000000000000..e6b557a4662a
--- /dev/null
+++ b/browser/metro/base/content/helperui/CapturePickerUI.js
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* appears to have something to do with capturing a camera image. tries to
+ * create a dialog, which will fail. TBD. */
+var CapturePickerUI = {
+ init: function() {
+ this.messageManager = Cc["@mozilla.org/parentprocessmessagemanager;1"].getService(Ci.nsIFrameMessageManager);
+ this.messageManager.addMessageListener("CapturePicker:Show", this);
+ },
+
+ receiveMessage: function(aMessage) {
+ switch (aMessage.name) {
+ case "CapturePicker:Show":
+ let params = { result: true };
+ let dialog = DialogUI.importModal(null, "chrome://browser/content/prompt/CaptureDialog.xul", params);
+ document.getElementById("capturepicker-title").appendChild(document.createTextNode(aMessage.json.title));
+ dialog.waitForClose();
+ return { value: params.result, path: params.path };
+ break;
+ }
+ // prevents warning from the script loader
+ return null;
+ }
+};
diff --git a/browser/metro/base/content/helperui/CharsetMenu.js b/browser/metro/base/content/helperui/CharsetMenu.js
new file mode 100644
index 000000000000..55012d1d5805
--- /dev/null
+++ b/browser/metro/base/content/helperui/CharsetMenu.js
@@ -0,0 +1,89 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var CharsetMenu = {
+ _strings: null,
+ _charsets: null,
+
+ get strings() {
+ if (!this._strings)
+ this._strings = Services.strings.createBundle("chrome://global/locale/charsetTitles.properties");
+ return this._strings;
+ },
+
+ init: function() {
+ PageActions.register("pageaction-charset", this.updatePageAction, this);
+ },
+
+ updatePageAction: function(aNode) {
+ let pref = Services.prefs.getComplexValue("browser.menu.showCharacterEncoding", Ci.nsIPrefLocalizedString).data;
+ if (pref == "true") {
+ let charset = getBrowser().docShell.forcedCharset;
+ if (charset) {
+ charset = charset.toString();
+ charset = charset.trim().toLowerCase();
+ aNode.setAttribute("description", this.strings.GetStringFromName(charset + ".title"));
+ } else if (aNode.hasAttribute("description")) {
+ aNode.removeAttribute("description");
+ }
+ }
+ return ("true" == pref)
+ },
+
+ _toMenuItems: function(aCharsets, aCurrent) {
+ let ret = [];
+ aCharsets.forEach(function (aSet) {
+ try {
+ let string = aSet.trim().toLowerCase();
+ ret.push({
+ label: this.strings.GetStringFromName(string + ".title"),
+ value: string,
+ selected: (string == aCurrent)
+ });
+ } catch(ex) { }
+ }, this);
+ return ret;
+ },
+
+ menu : {
+ dispatchEvent: function(aEvent) {
+ if (aEvent.type == "command")
+ CharsetMenu.setCharset(this.menupopup.children[this.selectedIndex].value);
+ },
+ menupopup: {
+ hasAttribute: function(aAttr) { return false; },
+ },
+ selectedIndex: -1
+ },
+
+ get charsets() {
+ if (!this._charsets) {
+ this._charsets = Services.prefs.getComplexValue("intl.charsetmenu.browser.static", Ci.nsIPrefLocalizedString).data.split(",");
+ }
+ let charsets = this._charsets;
+ let currentCharset = getBrowser().docShell.forcedCharset;
+
+ if (currentCharset) {
+ currentCharset = currentCharset.toString();
+ currentCharset = currentCharset.trim().toLowerCase();
+ if (charsets.indexOf(currentCharset) == -1)
+ charsets.splice(0, 0, currentCharset);
+ }
+ return this._toMenuItems(charsets, currentCharset);
+ },
+
+ show: function showCharsetMenu() {
+ this.menu.menupopup.children = this.charsets;
+ MenuControlHelperUI.show(this.menu);
+ },
+
+ setCharset: function setCharset(aCharset) {
+ let browser = getBrowser();
+ browser.messageManager.sendAsyncMessage("Browser:SetCharset", {
+ charset: aCharset
+ });
+ let history = Cc["@mozilla.org/browser/nav-history-service;1"].getService(Ci.nsINavHistoryService);
+ history.setCharsetForURI(browser.documentURI, aCharset);
+ }
+};
diff --git a/browser/metro/base/content/helperui/FindHelperUI.js b/browser/metro/base/content/helperui/FindHelperUI.js
new file mode 100644
index 000000000000..9ca5fb36d074
--- /dev/null
+++ b/browser/metro/base/content/helperui/FindHelperUI.js
@@ -0,0 +1,173 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var FindHelperUI = {
+ type: "find",
+ commands: {
+ next: "cmd_findNext",
+ previous: "cmd_findPrevious",
+ close: "cmd_findClose"
+ },
+
+ _open: false,
+ _status: null,
+
+ get status() {
+ return this._status;
+ },
+
+ set status(val) {
+ if (val != this._status) {
+ this._status = val;
+ if (!val)
+ this._textbox.removeAttribute("status");
+ else
+ this._textbox.setAttribute("status", val);
+ this.updateCommands(this._textbox.value);
+ }
+ },
+
+ init: function findHelperInit() {
+ this._textbox = document.getElementById("find-helper-textbox");
+ this._container = document.getElementById("content-navigator");
+
+ this._cmdPrevious = document.getElementById(this.commands.previous);
+ this._cmdNext = document.getElementById(this.commands.next);
+
+ // Listen for find assistant messages from content
+ messageManager.addMessageListener("FindAssist:Show", this);
+ messageManager.addMessageListener("FindAssist:Hide", this);
+
+ // Listen for pan events happening on the browsers
+ Elements.browsers.addEventListener("PanBegin", this, false);
+ Elements.browsers.addEventListener("PanFinished", this, false);
+
+ // Listen for events where form assistant should be closed
+ Elements.tabList.addEventListener("TabSelect", this, true);
+ Elements.browsers.addEventListener("URLChanged", this, true);
+ window.addEventListener("MozContextUIShow", this, true);
+ window.addEventListener("MozContextUIExpand", this, true);
+ },
+
+ receiveMessage: function findHelperReceiveMessage(aMessage) {
+ let json = aMessage.json;
+ switch(aMessage.name) {
+ case "FindAssist:Show":
+ ContextUI.dismiss();
+ this.status = json.result;
+ if (json.rect)
+ this._zoom(Rect.fromRect(json.rect));
+ break;
+
+ case "FindAssist:Hide":
+ if (this._container.getAttribute("type") == this.type)
+ this.hide();
+ break;
+ }
+ },
+
+ handleEvent: function findHelperHandleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "MozContextUIShow":
+ case "MozContextUIExpand":
+ case "TabSelect":
+ this.hide();
+ break;
+
+ case "URLChanged":
+ if (aEvent.detail && aEvent.target == getBrowser())
+ this.hide();
+ break;
+
+ case "PanBegin":
+ this._container.style.visibility = "hidden";
+ this._textbox.collapsed = true;
+ break;
+
+ case "PanFinished":
+ this._container.style.visibility = "visible";
+ this._textbox.collapsed = false;
+ break;
+ }
+ },
+
+ show: function findHelperShow() {
+
+ ContextUI.dismiss();
+
+ this._container.show(this);
+ this.search(this._textbox.value);
+ this._textbox.select();
+ this._textbox.focus();
+ this._open = true;
+
+ // Prevent the view to scroll automatically while searching
+ Browser.selectedBrowser.scrollSync = false;
+ },
+
+ hide: function findHelperHide() {
+ if (!this._open)
+ return;
+
+ this._textbox.value = "";
+ this.status = null;
+ this._textbox.blur();
+ this._container.hide(this);
+ this._open = false;
+
+ // Restore the scroll synchronisation
+ Browser.selectedBrowser.scrollSync = true;
+ },
+
+ goToPrevious: function findHelperGoToPrevious() {
+ Browser.selectedBrowser.messageManager.sendAsyncMessage("FindAssist:Previous", { });
+ },
+
+ goToNext: function findHelperGoToNext() {
+ Browser.selectedBrowser.messageManager.sendAsyncMessage("FindAssist:Next", { });
+ },
+
+ search: function findHelperSearch(aValue) {
+ this.updateCommands(aValue);
+
+ // Don't bother searching if the value is empty
+ if (aValue == "") {
+ this.status = null;
+ return;
+ }
+
+ Browser.selectedBrowser.messageManager.sendAsyncMessage("FindAssist:Find", { searchString: aValue });
+ },
+
+ updateCommands: function findHelperUpdateCommands(aValue) {
+ let disabled = (this._status == Ci.nsITypeAheadFind.FIND_NOTFOUND) || (aValue == "");
+ this._cmdPrevious.setAttribute("disabled", disabled);
+ this._cmdNext.setAttribute("disabled", disabled);
+ },
+
+ _zoom: function _findHelperZoom(aElementRect) {
+ let autozoomEnabled = Services.prefs.getBoolPref("findhelper.autozoom");
+ if (!aElementRect || !autozoomEnabled)
+ return;
+
+ if (Browser.selectedTab.allowZoom) {
+ let zoomLevel = Browser._getZoomLevelForRect(aElementRect);
+
+ // Clamp the zoom level relatively to the default zoom level of the page
+ let defaultZoomLevel = Browser.selectedTab.getDefaultZoomLevel();
+ zoomLevel = Util.clamp(zoomLevel, (defaultZoomLevel * kBrowserFormZoomLevelMin),
+ (defaultZoomLevel * kBrowserFormZoomLevelMax));
+ zoomLevel = Browser.selectedTab.clampZoomLevel(zoomLevel);
+
+ let zoomRect = Browser._getZoomRectForPoint(aElementRect.center().x, aElementRect.y, zoomLevel);
+ AnimatedZoom.animateTo(zoomRect);
+ } else {
+ // Even if zooming is disabled we could need to reposition the view in
+ // order to keep the element on-screen
+ let zoomRect = Browser._getZoomRectForPoint(aElementRect.center().x, aElementRect.y, getBrowser().scale);
+ AnimatedZoom.animateTo(zoomRect);
+ }
+ }
+};
diff --git a/browser/metro/base/content/helperui/FormHelperUI.js b/browser/metro/base/content/helperui/FormHelperUI.js
new file mode 100644
index 000000000000..2eba46eaa246
--- /dev/null
+++ b/browser/metro/base/content/helperui/FormHelperUI.js
@@ -0,0 +1,454 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Responsible for filling in form information.
+ * - When an element is focused, the browser view zooms in to the control.
+ * - The caret positionning and the view are sync to keep the type
+ * in text into view for input fields (text/textarea).
+ * - Provides autocomplete box for input fields.
+ */
+
+const kBrowserFormZoomLevelMin = 0.8;
+const kBrowserFormZoomLevelMax = 2.0;
+
+// Prefs
+const kPrefFormHelperEnabled = "formhelper.enabled";
+const kPrefFormHelperMode = "formhelper.mode";
+const kPrefFormHelperZoom = "formhelper.autozoom";
+const kPrefFormHelperZoomCaret = "formhelper.autozoom.caret";
+
+var FormHelperUI = {
+ _debugEvents: false,
+ _currentBrowser: null,
+ _currentElement: null,
+ _currentCaretRect: null,
+ _currentElementRect: null,
+
+ type: "form",
+
+ get enabled() {
+ return Services.prefs.getBoolPref(kPrefFormHelperEnabled);
+ },
+
+ init: function formHelperInit() {
+ // Listen for form assistant messages from content
+ messageManager.addMessageListener("FormAssist:Show", this);
+ messageManager.addMessageListener("FormAssist:Hide", this);
+ messageManager.addMessageListener("FormAssist:Update", this);
+ messageManager.addMessageListener("FormAssist:Resize", this);
+ messageManager.addMessageListener("FormAssist:AutoComplete", this);
+ messageManager.addMessageListener("FormAssist:ValidationMessage", this);
+
+ // Listen for events where form assistant should be closed or updated
+ let tabs = Elements.tabList;
+ tabs.addEventListener("TabSelect", this, true);
+ tabs.addEventListener("TabClose", this, true);
+ Elements.browsers.addEventListener("URLChanged", this, true);
+ Elements.browsers.addEventListener("SizeChanged", this, true);
+
+ // Listen some events to show/hide arrows
+ Elements.browsers.addEventListener("PanBegin", this, false);
+ Elements.browsers.addEventListener("PanFinished", this, false);
+
+ // Dynamically enabled/disabled the form helper if needed depending on
+ // the size of the screen
+ let mode = Services.prefs.getIntPref(kPrefFormHelperMode);
+ let state = (mode == 2) ? false : !!mode;
+ Services.prefs.setBoolPref(kPrefFormHelperEnabled, state);
+ },
+
+ /*
+ * Open of the form helper proper. Currently doesn't display anything
+ * on metro since the nav buttons are off.
+ */
+ show: function formHelperShow(aElement, aHasPrevious, aHasNext) {
+ // Delay the operation until all resize operations generated by the
+ // keyboard apparition are done. This avoid doing unuseful zooming
+ // operations.
+ if (aElement.editable &&
+ (MetroUtils.immersive && !ContentAreaObserver.isKeyboardOpened &&
+ !InputSourceHelper.isPrecise)) {
+ this._waitForKeyboard(aElement, aHasPrevious, aHasNext);
+ return;
+ }
+
+ this._currentBrowser = Browser.selectedBrowser;
+ this._currentCaretRect = null;
+
+ let lastElement = this._currentElement || null;
+
+ this._currentElement = {
+ id: aElement.id,
+ name: aElement.name,
+ title: aElement.title,
+ value: aElement.value,
+ maxLength: aElement.maxLength,
+ type: aElement.type,
+ choices: aElement.choices,
+ isAutocomplete: aElement.isAutocomplete,
+ validationMessage: aElement.validationMessage,
+ list: aElement.list,
+ rect: aElement.rect
+ };
+
+ this._zoom(Rect.fromRect(aElement.rect), Rect.fromRect(aElement.caretRect));
+ this._updateContainerForSelect(lastElement, this._currentElement);
+ this._updatePopupsFor(this._currentElement);
+
+ // Prevent the view to scroll automatically while typing
+ this._currentBrowser.scrollSync = false;
+ },
+
+ hide: function formHelperHide() {
+ SelectHelperUI.hide();
+ AutofillMenuUI.hide();
+
+ // Restore the scroll synchonisation
+ if (this._currentBrowser)
+ this._currentBrowser.scrollSync = true;
+
+ // reset current Element and Caret Rect
+ this._currentElementRect = null;
+ this._currentCaretRect = null;
+
+ this._updateContainerForSelect(this._currentElement, null);
+ if (this._currentBrowser)
+ this._currentBrowser.messageManager.sendAsyncMessage("FormAssist:Closed", { });
+ },
+
+ /*
+ * Events
+ */
+
+ handleEvent: function formHelperHandleEvent(aEvent) {
+ if (this._debugEvents) Util.dumpLn(aEvent.type);
+
+ if (!this._open)
+ return;
+
+ switch (aEvent.type) {
+ case "TabSelect":
+ case "TabClose":
+ case "PanBegin":
+ case "SizeChanged":
+ this.hide();
+ break;
+
+ case "URLChanged":
+ if (aEvent.detail && aEvent.target == getBrowser())
+ this.hide();
+ break;
+ }
+ },
+
+ receiveMessage: function formHelperReceiveMessage(aMessage) {
+ if (this._debugEvents) Util.dumpLn(aMessage.name);
+ let allowedMessages = ["FormAssist:Show", "FormAssist:Hide",
+ "FormAssist:AutoComplete", "FormAssist:ValidationMessage"];
+ if (!this._open && allowedMessages.indexOf(aMessage.name) == -1)
+ return;
+
+ let json = aMessage.json;
+ switch (aMessage.name) {
+ case "FormAssist:Show":
+ // if the user has manually disabled the Form Assistant UI we still
+ // want to show a UI for element and still want to show
+ // autocomplete suggestions but not managed by FormHelperUI
+ if (this.enabled) {
+ this.show(json.current, json.hasPrevious, json.hasNext)
+ } else if (json.current.choices) {
+ SelectHelperUI.show(json.current.choices, json.current.title, json.current.rect);
+ } else {
+ this._currentElementRect = Rect.fromRect(json.current.rect);
+ this._currentBrowser = getBrowser();
+ this._updatePopupsFor(json.current);
+ }
+ break;
+
+ case "FormAssist:Hide":
+ if (this.enabled) {
+ this.hide();
+ }
+ break;
+
+ case "FormAssist:Resize":
+ if (!ContentAreaObserver.isKeyboardOpened)
+ return;
+
+ let element = json.current;
+ this._zoom(Rect.fromRect(element.rect), Rect.fromRect(element.caretRect));
+ break;
+
+ case "FormAssist:ValidationMessage":
+ this._updatePopupsFor(json.current, { fromInput: true });
+ break;
+
+ case "FormAssist:AutoComplete":
+ this._updatePopupsFor(json.current, { fromInput: true });
+ break;
+
+ case "FormAssist:Update":
+ if (!ContentAreaObserver.isKeyboardOpened)
+ return;
+ //this._zoom(null, Rect.fromRect(json.caretRect));
+ break;
+ }
+ },
+
+ doAutoComplete: function formHelperDoAutoComplete(aData) {
+ this._currentBrowser.messageManager.sendAsyncMessage("FormAssist:AutoComplete",
+ { value: aData });
+ },
+
+ get _open() {
+ // XXX we don't have the ability to test zooming
+ return true;
+ },
+
+ /*
+ * Update all popups based on the type of form element we are
+ * dealing with.
+ */
+ _updatePopupsFor: function _formHelperUpdatePopupsFor(aElement, options) {
+ options = options || {};
+
+ let fromInput = 'fromInput' in options && options.fromInput;
+
+ // The order of the updates matters here. If the popup update was
+ // triggered from user input (e.g. key press in an input element),
+ // we first check if there are input suggestions then check for
+ // a validation message. The idea here is that the validation message
+ // will be shown straight away once the invalid element is focused
+ // and suggestions will be shown as user inputs data. Only one popup
+ // is shown at a time. If both are not shown, then we ensure any
+ // previous popups are hidden.
+ let noPopupsShown = fromInput ?
+ (!this._updateSuggestionsFor(aElement) &&
+ !this._updateFormValidationFor(aElement)) :
+ (!this._updateFormValidationFor(aElement) &&
+ !this._updateSuggestionsFor(aElement));
+
+ if (noPopupsShown) {
+ AutofillMenuUI.hide();
+ }
+ },
+
+ /*
+ * Populates the autofill menu for this element.
+ */
+ _updateSuggestionsFor: function _formHelperUpdateSuggestionsFor(aElement) {
+ let suggestions = this._getAutocompleteSuggestions(aElement);
+ if (!suggestions.length)
+ return false;
+
+ // the scrollX/scrollY position can change because of the animated zoom so
+ // delay the suggestions positioning
+ /*
+ if (AnimatedZoom.isZooming()) {
+ let self = this;
+ this._waitForZoom(function() {
+ self._updateSuggestionsFor(aElement);
+ });
+ return true;
+ }
+ */
+
+ AutofillMenuUI.show(this._currentElementRect, suggestions);
+ return true;
+ },
+
+ _updateFormValidationFor: function _formHelperUpdateFormValidationFor(aElement) {
+ if (!aElement.validationMessage)
+ return false;
+ /*
+ // the scrollX/scrollY position can change because of the animated zoom so
+ // delay the suggestions positioning
+ if (AnimatedZoom.isZooming()) {
+ let self = this;
+ this._waitForZoom(function() {
+ self._updateFormValidationFor(aElement);
+ });
+ return true;
+ }
+
+ let validationContainer = document.getElementById("form-helper-validation-container");
+
+ // Update label with form validation message
+ validationContainer.firstChild.value = aElement.validationMessage;
+
+ ContentPopupHelper.popup = validationContainer;
+ ContentPopupHelper.anchorTo(this._currentElementRect);
+ */
+
+ return false;
+ },
+
+ /*
+ * Retrieve the autocomplete list from the autocomplete service for an element
+ */
+ _getAutocompleteSuggestions: function _formHelperGetAutocompleteSuggestions(aElement) {
+ if (!aElement.isAutocomplete) {
+ return [];
+ }
+
+ let suggestions = [];
+
+ let autocompleteService = Cc["@mozilla.org/satchel/form-autocomplete;1"].getService(Ci.nsIFormAutoComplete);
+ let results = autocompleteService.autoCompleteSearch(aElement.name || aElement.id, aElement.value, aElement, null);
+ if (results.matchCount > 0) {
+ for (let i = 0; i < results.matchCount; i++) {
+ let value = results.getValueAt(i);
+
+ // Do not show the value if it is the current one in the input field
+ if (value == aElement.value)
+ continue;
+
+ suggestions.push({ "label": value, "value": value});
+ }
+ }
+
+ // Add the datalist elements provided by the website, note that the
+ // displayed value can differ from the real value of the element.
+ let options = aElement.list;
+ for (let i = 0; i < options.length; i++)
+ suggestions.push(options[i]);
+
+ return suggestions;
+ },
+
+ /*
+ * Setup for displaying the selection choices menu
+ */
+ _updateContainerForSelect: function _formHelperUpdateContainerForSelect(aLastElement, aCurrentElement) {
+ let lastHasChoices = aLastElement && (aLastElement.choices != null);
+ let currentHasChoices = aCurrentElement && (aCurrentElement.choices != null);
+
+ if (currentHasChoices)
+ SelectHelperUI.show(aCurrentElement.choices, aCurrentElement.title, aCurrentElement.rect);
+ else if (lastHasChoices)
+ SelectHelperUI.hide();
+ },
+
+ /*
+ * Zoom and move viewport so that element is legible and touchable.
+ */
+ _zoom: function _formHelperZoom(aElementRect, aCaretRect) {
+ let browser = getBrowser();
+ let zoomRect = Rect.fromRect(browser.getBoundingClientRect());
+
+ this._currentElementRect = aElementRect;
+
+ // Zoom to a specified Rect
+ let autozoomEnabled = Services.prefs.getBoolPref(kPrefFormHelperZoom);
+ if (aElementRect && Browser.selectedTab.allowZoom && autozoomEnabled) {
+ this._currentElementRect = aElementRect;
+
+ // Zoom to an element by keeping the caret into view
+ let zoomLevel = Browser.selectedTab.clampZoomLevel(this._getZoomLevelForRect(aElementRect));
+
+ zoomRect = Browser._getZoomRectForPoint(aElementRect.center().x, aElementRect.y, zoomLevel);
+ AnimatedZoom.animateTo(zoomRect);
+ } else if (aElementRect && !Browser.selectedTab.allowZoom && autozoomEnabled) {
+ this._currentElementRect = aElementRect;
+
+ // Even if zooming is disabled we could need to reposition the view in
+ // order to keep the element on-screen
+ zoomRect = Browser._getZoomRectForPoint(aElementRect.center().x, aElementRect.y, browser.scale);
+ AnimatedZoom.animateTo(zoomRect);
+ }
+
+ this._ensureCaretVisible(aCaretRect);
+ },
+
+ _ensureCaretVisible: function _ensureCaretVisible(aCaretRect) {
+ if (!aCaretRect || !Services.prefs.getBoolPref(kPrefFormHelperZoomCaret))
+ return;
+
+ // the scrollX/scrollY position can change because of the animated zoom so
+ // delay the caret adjustment
+ if (AnimatedZoom.isZooming()) {
+ let self = this;
+ this._waitForZoom(function() {
+ self._ensureCaretVisible(aCaretRect);
+ });
+ return;
+ }
+
+ let browser = getBrowser();
+ let zoomRect = Rect.fromRect(browser.getBoundingClientRect());
+
+ this._currentCaretRect = aCaretRect;
+ let caretRect = aCaretRect.clone().scale(browser.scale, browser.scale);
+
+ let scroll = browser.getRootView().getPosition();
+ zoomRect = new Rect(scroll.x, scroll.y, zoomRect.width, zoomRect.height);
+ if (zoomRect.contains(caretRect))
+ return;
+
+ let [deltaX, deltaY] = this._getOffsetForCaret(caretRect, zoomRect);
+ if (deltaX != 0 || deltaY != 0) {
+ let view = browser.getRootView();
+ view.scrollBy(deltaX, deltaY);
+ }
+ },
+
+ _waitForZoom: function _formHelperWaitForZoom(aCallback) {
+ let currentElement = this._currentElement;
+ let self = this;
+ window.addEventListener("AnimatedZoomEnd", function() {
+ window.removeEventListener("AnimatedZoomEnd", arguments.callee, true);
+ // Ensure the current element has not changed during this interval
+ if (self._currentElement != currentElement)
+ return;
+
+ aCallback();
+ }, true);
+ },
+
+ _getZoomLevelForRect: function _getZoomLevelForRect(aRect) {
+ const margin = 30;
+ let zoomLevel = getBrowser().getBoundingClientRect().width / (aRect.width + margin);
+
+ // Clamp the zoom level relatively to the default zoom level of the page
+ let defaultZoomLevel = Browser.selectedTab.getDefaultZoomLevel();
+ return Util.clamp(zoomLevel, (defaultZoomLevel * kBrowserFormZoomLevelMin),
+ (defaultZoomLevel * kBrowserFormZoomLevelMax));
+ },
+
+ _getOffsetForCaret: function _formHelperGetOffsetForCaret(aCaretRect, aRect) {
+ // Determine if we need to move left or right to bring the caret into view
+ let deltaX = 0;
+ if (aCaretRect.right > aRect.right)
+ deltaX = aCaretRect.right - aRect.right;
+ if (aCaretRect.left < aRect.left)
+ deltaX = aCaretRect.left - aRect.left;
+
+ // Determine if we need to move up or down to bring the caret into view
+ let deltaY = 0;
+ if (aCaretRect.bottom > aRect.bottom)
+ deltaY = aCaretRect.bottom - aRect.bottom;
+ if (aCaretRect.top < aRect.top)
+ deltaY = aCaretRect.top - aRect.top;
+
+ return [deltaX, deltaY];
+ },
+
+ _waitForKeyboard: function formHelperWaitForKeyboard(aElement, aHasPrevious, aHasNext) {
+ let self = this;
+ window.addEventListener("KeyboardChanged", function(aEvent) {
+ window.removeEventListener("KeyboardChanged", arguments.callee, false);
+
+ if (AnimatedZoom.isZooming()) {
+ self._waitForZoom(function() {
+ self.show(aElement, aHasPrevious, aHasNext);
+ });
+ return;
+ }
+
+ self.show(aElement, aHasPrevious, aHasNext);
+ }, false);
+ }
+};
+
diff --git a/browser/metro/base/content/helperui/IdentityUI.js b/browser/metro/base/content/helperui/IdentityUI.js
new file mode 100644
index 000000000000..8f3beb372b33
--- /dev/null
+++ b/browser/metro/base/content/helperui/IdentityUI.js
@@ -0,0 +1,289 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Utility class to handle manipulations of the identity indicators in the UI
+ */
+
+var IdentityUI = {
+ // Mode strings used to control CSS display
+ IDENTITY_MODE_IDENTIFIED : "verifiedIdentity", // High-quality identity information
+ IDENTITY_MODE_DOMAIN_VERIFIED : "verifiedDomain", // Minimal SSL CA-signed domain verification
+ IDENTITY_MODE_UNKNOWN : "unknownIdentity", // No trusted identity information
+
+ // Cache the most recent SSLStatus and Location seen in checkIdentity
+ _lastStatus : null,
+ _lastLocation : null,
+
+ /**
+ * Build out a cache of the elements that we need frequently.
+ */
+ _cacheElements: function() {
+ this._identityBox = document.getElementById("identity-box");
+ this._identityPopup = document.getElementById("identity-container");
+ this._identityPopupContentBox = document.getElementById("identity-popup-content-box");
+ this._identityPopupContentHost = document.getElementById("identity-popup-content-host");
+ this._identityPopupContentOwner = document.getElementById("identity-popup-content-owner");
+ this._identityPopupContentSupp = document.getElementById("identity-popup-content-supplemental");
+ this._identityPopupContentVerif = document.getElementById("identity-popup-content-verifier");
+ this._identityPopupEncLabel = document.getElementById("identity-popup-encryption-label");
+ this._identityIcon = document.getElementById("identity-icon");
+ this._identityIconLabel = document.getElementById("identity-icon-label");
+ this._identityIconCountryLabel = document.getElementById("identity-icon-country-label");
+ },
+
+ init: function init() {
+ this._staticStrings = {};
+ this._staticStrings[this.IDENTITY_MODE_DOMAIN_VERIFIED] = {
+ encryption_label: Strings.browser.GetStringFromName("identity.encrypted2")
+ };
+ this._staticStrings[this.IDENTITY_MODE_IDENTIFIED] = {
+ encryption_label: Strings.browser.GetStringFromName("identity.encrypted2")
+ };
+ this._staticStrings[this.IDENTITY_MODE_UNKNOWN] = {
+ encryption_label: Strings.browser.GetStringFromName("identity.unencrypted2")
+ };
+
+ // Close the popup when reloading the page
+ Elements.browsers.addEventListener("URLChanged", this, true);
+
+ this._cacheElements();
+ },
+
+ getIdentityData: function() {
+ return this._lastStatus.serverCert;
+ },
+
+ /**
+ * Determine the identity of the page being displayed by examining its SSL cert
+ * (if available) and, if necessary, update the UI to reflect this.
+ */
+ checkIdentity: function() {
+ let browser = getBrowser();
+ let state = browser.securityUI.state;
+ let location = browser.currentURI;
+ let currentStatus = browser.securityUI.SSLStatus;
+
+ this._lastStatus = currentStatus;
+ this._lastLocation = {};
+
+ try {
+ // make a copy of the passed in location to avoid cycles
+ this._lastLocation = { host: location.hostPort, hostname: location.host, port: location.port == -1 ? "" : location.port};
+ } catch(e) { }
+
+ if (state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL)
+ this.setMode(this.IDENTITY_MODE_IDENTIFIED);
+ else if (state & Ci.nsIWebProgressListener.STATE_SECURE_HIGH)
+ this.setMode(this.IDENTITY_MODE_DOMAIN_VERIFIED);
+ else
+ this.setMode(this.IDENTITY_MODE_UNKNOWN);
+ },
+
+ /**
+ * Return the eTLD+1 version of the current hostname
+ */
+ getEffectiveHost: function() {
+ // Cache the eTLDService if this is our first time through
+ if (!this._eTLDService)
+ this._eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"]
+ .getService(Ci.nsIEffectiveTLDService);
+ try {
+ return this._eTLDService.getBaseDomainFromHost(this._lastLocation.hostname);
+ } catch (e) {
+ // If something goes wrong (e.g. hostname is an IP address) just fail back
+ // to the full domain.
+ return this._lastLocation.hostname;
+ }
+ },
+
+ /**
+ * Update the UI to reflect the specified mode, which should be one of the
+ * IDENTITY_MODE_* constants.
+ */
+ setMode: function(newMode) {
+ this._identityBox.setAttribute("mode", newMode);
+ this.setIdentityMessages(newMode);
+
+ // Update the popup too, if it's open
+ if (!this._identityPopup.hidden)
+ this.setPopupMessages(newMode);
+ },
+
+ /**
+ * Set up the messages for the primary identity UI based on the specified mode,
+ * and the details of the SSL cert, where applicable
+ *
+ * @param newMode The newly set identity mode. Should be one of the IDENTITY_MODE_* constants.
+ */
+ setIdentityMessages: function(newMode) {
+ let strings = Strings.browser;
+ let tooltip = "";
+ let icon_country_label = "";
+ let icon_labels_dir = "ltr";
+ let icon_label = "";
+
+ if (newMode == this.IDENTITY_MODE_DOMAIN_VERIFIED) {
+ var iData = this.getIdentityData();
+
+ // We need a port number for all lookups. If one hasn't been specified, use
+ // the https default
+ var lookupHost = this._lastLocation.host;
+ if (lookupHost.indexOf(':') < 0)
+ lookupHost += ":443";
+
+ // Cache the override service the first time we need to check it
+ if (!this._overrideService)
+ this._overrideService = Cc["@mozilla.org/security/certoverride;1"].getService(Ci.nsICertOverrideService);
+
+ // Verifier is either the CA Org, for a normal cert, or a special string
+ // for certs that are trusted because of a security exception.
+ tooltip = strings.formatStringFromName("identity.identified.verifier", [iData.caOrg], 1);
+
+ // Check whether this site is a security exception.
+ if (iData.isException)
+ tooltip = strings.GetStringFromName("identity.identified.verified_by_you");
+ }
+ else if (newMode == this.IDENTITY_MODE_IDENTIFIED) {
+ // If it's identified, then we can populate the dialog with credentials
+ iData = this.getIdentityData();
+ tooltip = strings.formatStringFromName("identity.identified.verifier", [iData.caOrg], 1);
+
+ icon_label = iData.subjectOrg;
+ if (iData.country)
+ icon_country_label = "(" + iData.country + ")";
+
+ // If the organization name starts with an RTL character, then
+ // swap the positions of the organization and country code labels.
+ // The Unicode ranges reflect the definition of the UCS2_CHAR_IS_BIDI
+ // macro in intl/unicharutil/util/nsBidiUtils.h. When bug 218823 gets
+ // fixed, this test should be replaced by one adhering to the
+ // Unicode Bidirectional Algorithm proper (at the paragraph level).
+ icon_labels_dir = /^[\u0590-\u08ff\ufb1d-\ufdff\ufe70-\ufefc]/.test(icon_label) ?
+ "rtl" : "ltr";
+ }
+ else {
+ tooltip = strings.GetStringFromName("identity.unknown.tooltip");
+ }
+
+ // Push the appropriate strings out to the UI
+ this._identityBox.tooltipText = tooltip;
+ this._identityIconLabel.value = icon_label;
+ this._identityIconCountryLabel.value = icon_country_label;
+ // Set cropping and direction
+ this._identityIconLabel.crop = icon_country_label ? "end" : "center";
+ this._identityIconLabel.parentNode.style.direction = icon_labels_dir;
+ // Hide completely if the organization label is empty
+ this._identityIconLabel.parentNode.collapsed = icon_label ? false : true;
+ },
+
+ /**
+ * Set up the title and content messages for the identity message popup,
+ * based on the specified mode, and the details of the SSL cert, where
+ * applicable
+ *
+ * @param newMode The newly set identity mode. Should be one of the IDENTITY_MODE_* constants.
+ */
+ setPopupMessages: function(newMode) {
+ this._identityPopup.setAttribute("mode", newMode);
+ this._identityPopupContentBox.className = newMode;
+
+ // Set the static strings up front
+ this._identityPopupEncLabel.textContent = this._staticStrings[newMode].encryption_label;
+
+ // Initialize the optional strings to empty values
+ var supplemental = "";
+ var verifier = "";
+
+ if (newMode == this.IDENTITY_MODE_DOMAIN_VERIFIED) {
+ var iData = this.getIdentityData();
+ var host = this.getEffectiveHost();
+ var owner = Strings.browser.GetStringFromName("identity.ownerUnknown2");
+ verifier = this._identityBox.tooltipText;
+ supplemental = "";
+ }
+ else if (newMode == this.IDENTITY_MODE_IDENTIFIED) {
+ // If it's identified, then we can populate the dialog with credentials
+ iData = this.getIdentityData();
+ host = this.getEffectiveHost();
+ owner = iData.subjectOrg;
+ verifier = this._identityBox.tooltipText;
+
+ // Build an appropriate supplemental block out of whatever location data we have
+ if (iData.city)
+ supplemental += iData.city + "\n";
+ if (iData.state && iData.country)
+ supplemental += Strings.browser.formatStringFromName("identity.identified.state_and_country", [iData.state, iData.country], 2);
+ else if (iData.state) // State only
+ supplemental += iData.state;
+ else if (iData.country) // Country only
+ supplemental += iData.country;
+ } else {
+ // These strings will be hidden in CSS anyhow
+ host = "";
+ owner = "";
+ }
+
+ // Push the appropriate strings out to the UI
+ this._identityPopupContentHost.textContent = host;
+ this._identityPopupContentOwner.textContent = owner;
+ this._identityPopupContentSupp.textContent = supplemental;
+ this._identityPopupContentVerif.textContent = verifier;
+
+ PageActions.updateSiteMenu();
+ },
+
+ show: function ih_show() {
+ BrowserUI.showContent();
+
+ Elements.contentShowing.setAttribute("disabled", "true");
+
+ // Update the popup strings
+ this.setPopupMessages(this._identityBox.getAttribute("mode") || this.IDENTITY_MODE_UNKNOWN);
+
+ this._identityPopup.hidden = false;
+ this._identityBox.setAttribute("open", "true");
+
+ DialogUI.pushPopup(this, [this._identityPopup, this._identityBox, Elements.tray]);
+ this._identityPopup.anchorTo(this._identityBox, "after_start");
+ },
+
+ hide: function ih_hide() {
+ Elements.contentShowing.setAttribute("disabled", "false");
+
+ this._identityPopup.hidden = true;
+ this._identityBox.removeAttribute("open");
+
+ DialogUI.popPopup(this);
+ },
+
+ toggle: function ih_toggle() {
+ if (this._identityPopup.hidden)
+ this.show();
+ else
+ this.hide();
+ },
+
+ /**
+ * Click handler for the identity-box element in primary chrome.
+ */
+ handleIdentityButtonEvent: function(aEvent) {
+ aEvent.stopPropagation();
+
+ if ((aEvent.type == "click" && aEvent.button != 0) ||
+ (aEvent.type == "keypress" && aEvent.charCode != KeyEvent.DOM_VK_SPACE &&
+ aEvent.keyCode != KeyEvent.DOM_VK_RETURN))
+ return; // Left click, space or enter only
+
+ this.toggle();
+ },
+
+ handleEvent: function(aEvent) {
+ if (aEvent.type == "URLChanged" && aEvent.target == Browser.selectedBrowser && !this._identityPopup.hidden)
+ this.hide();
+ }
+};
+
+
+
diff --git a/browser/metro/base/content/helperui/IndexedDB.js b/browser/metro/base/content/helperui/IndexedDB.js
new file mode 100644
index 000000000000..8c347ab5e36f
--- /dev/null
+++ b/browser/metro/base/content/helperui/IndexedDB.js
@@ -0,0 +1,82 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Helper class for IndexedDB, parent part. Listens to
+ * messages from the child and shows prompts for them.
+ */
+let IndexedDB = {
+ _permissionsPrompt: "indexedDB-permissions-prompt",
+ _permissionsResponse: "indexedDB-permissions-response",
+
+ _quotaPrompt: "indexedDB-quota-prompt",
+ _quotaResponse: "indexedDB-quota-response",
+ _quotaCancel: "indexedDB-quota-cancel",
+
+ _notificationIcon: "indexedDB-notification-icon",
+
+ receiveMessage: function(aMessage) {
+ switch (aMessage.name) {
+ case "IndexedDB:Prompt":
+ this.showPrompt(aMessage);
+ }
+ },
+
+ showPrompt: function(aMessage) {
+ let browser = aMessage.target;
+ let payload = aMessage.json;
+ let host = payload.host;
+ let topic = payload.topic;
+ let type;
+
+ if (topic == this._permissionsPrompt) {
+ type = "indexedDB";
+ payload.responseTopic = this._permissionsResponse;
+ } else if (topic == this._quotaPrompt) {
+ type = "indexedDBQuota";
+ payload.responseTopic = this._quotaResponse;
+ } else if (topic == this._quotaCancel) {
+ payload.permission = Ci.nsIPermissionManager.UNKNOWN_ACTION;
+ browser.messageManager.sendAsyncMessage("IndexedDB:Response", payload);
+ // XXX Need to actually save this?
+ return;
+ }
+
+ let prompt = Cc["@mozilla.org/content-permission/prompt;1"].createInstance(Ci.nsIContentPermissionPrompt);
+
+ // If the user waits a long time before responding, we default to UNKNOWN_ACTION.
+ let timeoutId = setTimeout(function() {
+ payload.permission = Ci.nsIPermissionManager.UNKNOWN_ACTION;
+ browser.messageManager.sendAsyncMessage("IndexedDB:Response", payload);
+ timeoutId = null;
+ }, 30000);
+
+ function checkTimeout() {
+ if (timeoutId === null) return true;
+ clearTimeout(timeoutId);
+ timeoutId = null;
+ return false;
+ }
+
+ prompt.prompt({
+ type: type,
+ uri: Services.io.newURI(payload.location, null, null),
+ window: null,
+ element: aMessage.target,
+
+ cancel: function() {
+ if (checkTimeout()) return;
+ payload.permission = Ci.nsIPermissionManager.DENY_ACTION;
+ browser.messageManager.sendAsyncMessage("IndexedDB:Response", payload);
+ },
+
+ allow: function() {
+ if (checkTimeout()) return;
+ payload.permission = Ci.nsIPermissionManager.ALLOW_ACTION;
+ browser.messageManager.sendAsyncMessage("IndexedDB:Response", payload);
+ },
+ });
+ },
+};
+
diff --git a/browser/metro/base/content/helperui/MasterPasswordUI.js b/browser/metro/base/content/helperui/MasterPasswordUI.js
new file mode 100644
index 000000000000..b2229f999540
--- /dev/null
+++ b/browser/metro/base/content/helperui/MasterPasswordUI.js
@@ -0,0 +1,129 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var MasterPasswordUI = {
+ _dialog: null,
+ _tokenName: "",
+
+ get _secModuleDB() {
+ delete this._secModuleDB;
+ return this._secModuleDB = Cc["@mozilla.org/security/pkcs11moduledb;1"].getService(Ci.nsIPKCS11ModuleDB);
+ },
+
+ get _pk11DB() {
+ delete this._pk11DB;
+ return this._pk11DB = Cc["@mozilla.org/security/pk11tokendb;1"].getService(Ci.nsIPK11TokenDB);
+ },
+
+ _setPassword: function _setPassword(password) {
+ try {
+ let status;
+ let slot = this._secModuleDB.findSlotByName(this._tokenName);
+ if (slot)
+ status = slot.status;
+ else
+ return false;
+
+ let token = this._pk11DB.findTokenByName(this._tokenName);
+
+ if (status == Ci.nsIPKCS11Slot.SLOT_UNINITIALIZED) {
+ token.initPassword(password);
+ } else if (status == Ci.nsIPKCS11Slot.SLOT_READY) {
+ token.changePassword("", password);
+ }
+ return true;
+ } catch(e) {
+ dump("--- MasterPasswordUI._setPassword exception: " + e + "\n");
+ return false;
+ }
+ },
+
+ _removePassword: function _removePassword(password) {
+ try {
+ let token = this._pk11DB.getInternalKeyToken();
+ if (token.checkPassword(password)) {
+ token.changePassword(password, "");
+ return true;
+ }
+ } catch(e) {
+ dump("--- MasterPasswordUI._removePassword exception: " + e + "\n");
+ }
+ return false;
+ },
+
+ show: function mp_show(aSet) {
+ let dialogId = aSet ? "masterpassword-change" : "masterpassword-remove";
+ if (document.getElementById(dialogId))
+ return;
+
+ let dialog = aSet ? "chrome://browser/content/prompt/masterPassword.xul"
+ : "chrome://browser/content/prompt/removeMasterPassword.xul";
+ this._dialog = DialogUI.importModal(window, dialog, null);
+ DialogUI.pushPopup(this, this._dialog);
+
+ if (aSet) {
+ this.checkPassword();
+ document.getElementById("masterpassword-newpassword1").focus();
+ } else {
+ document.getElementById("masterpassword-oldpassword").focus();
+ }
+ },
+
+ hide: function mp_hide(aValue) {
+ this.updatePreference();
+ this._dialog.close();
+ this._dialog = null;
+ DialogUI.popPopup(this);
+ },
+
+ setPassword: function mp_setPassword() {
+ if (!this.checkPassword())
+ return;
+
+ let newPasswordValue = document.getElementById("masterpassword-newpassword1").value;
+ if (this._setPassword(newPasswordValue)) {
+ this.hide();
+ }
+ },
+
+ removePassword: function mp_removePassword() {
+ let oldPassword = document.getElementById("masterpassword-oldpassword").value;
+ if (this._removePassword(oldPassword)) {
+ this.hide();
+ }
+ },
+
+ checkPassword: function mp_checkPassword() {
+ let newPasswordValue1 = document.getElementById("masterpassword-newpassword1").value;
+ let newPasswordValue2 = document.getElementById("masterpassword-newpassword2").value;
+
+ let buttonOk = this._dialog.getElementsByAttribute("class", "prompt-buttons")[0].firstChild;
+ let isPasswordValid = this._secModuleDB.isFIPSEnabled ? (newPasswordValue1 != "" && newPasswordValue1 == newPasswordValue2)
+ : (newPasswordValue1 == newPasswordValue2);
+ buttonOk.setAttribute("disabled", !isPasswordValid);
+
+ return isPasswordValid;
+ },
+
+ checkOldPassword: function mp_checkOldPassword() {
+ let oldPassword = document.getElementById("masterpassword-oldpassword");
+
+ let buttonOk = this._dialog.getElementsByAttribute("class", "prompt-buttons")[0].firstChild;
+ let isPasswordValid = this._pk11DB.getInternalKeyToken().checkPassword(oldPassword.value);
+ buttonOk.setAttribute("disabled", !isPasswordValid);
+ },
+
+ hasMasterPassword: function mp_hasMasterPassword() {
+ let slot = this._secModuleDB.findSlotByName(this._tokenName);
+ if (slot) {
+ let status = slot.status;
+ return status != Ci.nsIPKCS11Slot.SLOT_UNINITIALIZED && status != Ci.nsIPKCS11Slot.SLOT_READY;
+ }
+ return false;
+ },
+
+ updatePreference: function mp_updatePreference() {
+ document.getElementById("prefs-master-password").value = this.hasMasterPassword();
+ }
+};
diff --git a/browser/metro/base/content/helperui/MenuUI.js b/browser/metro/base/content/helperui/MenuUI.js
new file mode 100644
index 000000000000..31469e9e5def
--- /dev/null
+++ b/browser/metro/base/content/helperui/MenuUI.js
@@ -0,0 +1,438 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// Positioning buffer enforced between the edge of a context menu
+// and the edge of the screen.
+const kPositionPadding = 10;
+
+var AutofillMenuUI = {
+ _popupState: null,
+ __menuPopup: null,
+
+ get _panel() { return document.getElementById("autofill-container"); },
+ get _popup() { return document.getElementById("autofill-popup"); },
+ get _commands() { return this._popup.childNodes[0]; },
+
+ get _menuPopup() {
+ if (!this.__menuPopup)
+ this.__menuPopup = new MenuPopup(this._panel, this._popup);
+ this.__menuPopup._wantTypeBehind = true;
+
+ return this.__menuPopup;
+ },
+
+ _firePopupEvent: function _firePopupEvent(aEventName) {
+ let menupopup = this._currentControl.menupopup;
+ if (menupopup.hasAttribute(aEventName)) {
+ let func = new Function("event", menupopup.getAttribute(aEventName));
+ func.call(this);
+ }
+ },
+
+ _emptyCommands: function _emptyCommands() {
+ while (this._commands.firstChild)
+ this._commands.removeChild(this._commands.firstChild);
+ },
+
+ _positionOptions: function _positionOptions() {
+ let options = {
+ forcePosition: true
+ };
+ options.xPos = this._anchorRect.x;
+ options.yPos = this._anchorRect.y + this._anchorRect.height;
+ options.bottomAligned = false;
+ options.leftAligned = true;
+ options.source = Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH;
+ return options;
+ },
+
+ show: function show(aAnchorRect, aSuggestionsList) {
+ this._anchorRect = aAnchorRect;
+ this._emptyCommands();
+ for (let idx = 0; idx < aSuggestionsList.length; idx++) {
+ let item = document.createElement("richlistitem");
+ let label = document.createElement("label");
+ label.setAttribute("value", aSuggestionsList[idx].label);
+ item.setAttribute("data", aSuggestionsList[idx].value);
+ item.appendChild(label);
+ this._commands.appendChild(item);
+ }
+
+ this._menuPopup.show(this._positionOptions());
+ },
+
+ selectByIndex: function mn_selectByIndex(aIndex) {
+ this._menuPopup.hide();
+ FormHelperUI.doAutoComplete(this._commands.childNodes[aIndex].getAttribute("data"));
+ },
+
+ hide: function hide () {
+ this._menuPopup.hide();
+ }
+};
+
+var ContextMenuUI = {
+ _popupState: null,
+ __menuPopup: null,
+
+ get _panel() { return document.getElementById("context-container"); },
+ get _popup() { return document.getElementById("context-popup"); },
+ get _commands() { return this._popup.childNodes[0]; },
+
+ get _menuPopup() {
+ if (!this.__menuPopup)
+ this.__menuPopup = new MenuPopup(this._panel, this._popup);
+
+ return this.__menuPopup;
+ },
+
+ /*******************************************
+ * External api
+ */
+
+ /*
+ * popupState - return the json object for this context. Called
+ * by context command to invoke actions on the target.
+ */
+ get popupState() {
+ return this._popupState;
+ },
+
+ /*
+ * showContextMenu - display a context sensitive menu based
+ * on the data provided in a json data structure.
+ *
+ * @param aMessage data structure containing information about
+ * the context.
+ * aMessage.json - json data structure described below.
+ * aMessage.target - target element on which to evoke
+ *
+ * @returns true if the context menu was displayed,
+ * false otherwise.
+ *
+ * json: TBD
+ */
+ showContextMenu: function ch_showContextMenu(aMessage) {
+ this._popupState = aMessage.json;
+ this._popupState.target = aMessage.target;
+
+ let contentTypes = this._popupState.types;
+
+ let optionsAvailable = false;
+ for (let i = 0; i < this._commands.childElementCount; i++) {
+ let command = this._commands.childNodes[i];
+ command.hidden = true;
+
+ let types = command.getAttribute("type").split(/\s+/);
+ for (let i = 0; i < types.length; i++) {
+ if (contentTypes.indexOf(types[i]) != -1) {
+ optionsAvailable = true;
+ command.hidden = false;
+ break;
+ }
+ }
+ }
+
+ if (!optionsAvailable) {
+ this._popupState = null;
+ return false;
+ }
+
+
+ this._menuPopup.show(this._popupState);
+
+ let event = document.createEvent("Events");
+ event.initEvent("CancelTouchSequence", true, false);
+ if (this._popupState.target) {
+ this._popupState.target.dispatchEvent(event);
+ }
+ return true;
+ },
+
+ hide: function hide () {
+ this._menuPopup.hide();
+ this._popupState = null;
+ },
+
+ reset: function reset() {
+ this._popupState = null;
+ }
+};
+
+var MenuControlUI = {
+ _currentControl: null,
+ __menuPopup: null,
+
+ get _panel() { return document.getElementById("menucontrol-container"); },
+ get _popup() { return document.getElementById("menucontrol-popup"); },
+ get _commands() { return this._popup.childNodes[0]; },
+
+ get _menuPopup() {
+ if (!this.__menuPopup)
+ this.__menuPopup = new MenuPopup(this._panel, this._popup);
+
+ return this.__menuPopup;
+ },
+
+ _firePopupEvent: function _firePopupEvent(aEventName) {
+ let menupopup = this._currentControl.menupopup;
+ if (menupopup.hasAttribute(aEventName)) {
+ let func = new Function("event", menupopup.getAttribute(aEventName));
+ func.call(this);
+ }
+ },
+
+ _emptyCommands: function _emptyCommands() {
+ while (this._commands.firstChild)
+ this._commands.removeChild(this._commands.firstChild);
+ },
+
+ _positionOptions: function _positionOptions() {
+ let position = this._currentControl.menupopup.position || "after_start";
+ let rect = this._currentControl.getBoundingClientRect();
+
+ let options = {
+ forcePosition: true
+ };
+
+ // TODO: Detect text direction and flip for RTL.
+
+ switch (position) {
+ case "before_start":
+ options.xPos = rect.left;
+ options.yPos = rect.top;
+ options.bottomAligned = true;
+ options.leftAligned = true;
+ break;
+ case "before_end":
+ options.xPos = rect.right;
+ options.yPos = rect.top;
+ options.bottomAligned = true;
+ options.rightAligned = true;
+ break;
+ case "after_start":
+ options.xPos = rect.left;
+ options.yPos = rect.bottom;
+ options.topAligned = true;
+ options.leftAligned = true;
+ break;
+ case "after_end":
+ options.xPos = rect.right;
+ options.yPos = rect.bottom;
+ options.topAligned = true;
+ options.rightAligned = true;
+ break;
+
+ // TODO: Support other popup positions.
+ }
+
+ return options;
+ },
+
+ show: function show(aMenuControl) {
+ this._currentControl = aMenuControl;
+ this._panel.setAttribute("for", aMenuControl.id);
+ this._firePopupEvent("onpopupshowing");
+
+ this._emptyCommands();
+ let children = this._currentControl.menupopup.children;
+ for (let i = 0; i < children.length; i++) {
+ let child = children[i];
+ let item = document.createElement("richlistitem");
+
+ if (child.disabled)
+ item.setAttribute("disabled", "true");
+
+ if (child.hidden)
+ item.setAttribute("hidden", "true");
+
+ // Add selected as a class name instead of an attribute to not being overidden
+ // by the richlistbox behavior (it sets the "current" and "selected" attribute
+ if (child.selected)
+ item.setAttribute("class", "selected");
+
+ let image = document.createElement("image");
+ image.setAttribute("src", child.image || "");
+ item.appendChild(image);
+
+ let label = document.createElement("label");
+ label.setAttribute("value", child.label);
+ item.appendChild(label);
+
+ this._commands.appendChild(item);
+ }
+
+ this._menuPopup.show(this._positionOptions());
+ },
+
+ selectByIndex: function mn_selectByIndex(aIndex) {
+ this._currentControl.selectedIndex = aIndex;
+
+ // Dispatch a xul command event to the attached menulist
+ if (this._currentControl.dispatchEvent) {
+ let evt = document.createEvent("XULCommandEvent");
+ evt.initCommandEvent("command", true, true, window, 0, false, false, false, false, null);
+ this._currentControl.dispatchEvent(evt);
+ }
+
+ this._menuPopup.hide();
+ }
+};
+
+function MenuPopup(aPanel, aPopup) {
+ this._panel = aPanel;
+ this._popup = aPopup;
+ this._wantTypeBehind = false;
+}
+
+MenuPopup.prototype = {
+ get _visible() { return !this._panel.hidden; },
+ get _commands() { return this._popup.childNodes[0]; },
+
+ show: function (aPositionOptions) {
+ if (this._visible)
+ return;
+
+ window.addEventListener("keypress", this, true);
+ window.addEventListener("mousedown", this, true);
+
+ this._panel.hidden = false;
+ this._position(aPositionOptions || {});
+
+ let self = this;
+ this._panel.addEventListener("transitionend", function () {
+ self._panel.removeEventListener("transitionend", arguments.callee);
+ self._panel.removeAttribute("showingfrom");
+ });
+
+ let popupFrom = (aPositionOptions.forcePosition && !aPositionOptions.bottomAligned) ? "above" : "below";
+ this._panel.setAttribute("showingfrom", popupFrom);
+
+ // Ensure the panel actually gets shifted before getting animated
+ setTimeout(function () {
+ self._panel.setAttribute("showing", "true");
+ }, 0);
+ },
+
+ hide: function () {
+ if (!this._visible)
+ return;
+
+ window.removeEventListener("keypress", this, true);
+ window.removeEventListener("mousedown", this, true);
+
+ let self = this;
+ this._panel.addEventListener("transitionend", function () {
+ self._panel.removeEventListener("transitionend", arguments.callee);
+ self._panel.hidden = true;
+ self._popupState = null;
+ });
+
+ this._panel.removeAttribute("showing");
+ },
+
+ _position: function _position(aPositionOptions) {
+ let aX = aPositionOptions.xPos;
+ let aY = aPositionOptions.yPos;
+ let aSource = aPositionOptions.source;
+ let forcePosition = aPositionOptions.forcePosition || false;
+ let isRightAligned = aPositionOptions.rightAligned || false;
+ let isBottomAligned = aPositionOptions.bottomAligned || false;
+
+ let width = this._popup.boxObject.width;
+ let height = this._popup.boxObject.height;
+ let halfWidth = width / 2;
+ let halfHeight = height / 2;
+ let screenWidth = ContentAreaObserver.width;
+ let screenHeight = ContentAreaObserver.height;
+
+ if (forcePosition) {
+ if (isRightAligned)
+ aX -= width;
+
+ if (isBottomAligned)
+ aY -= height;
+ } else {
+ let leftHand = MetroUtils.handPreference == MetroUtils.handPreferenceLeft;
+
+ // Add padding on the side of the menu per the user's hand preference
+ if (aSource && aSource == Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH) {
+ if (leftHand) {
+ this._commands.setAttribute("left-hand", true);
+ this._commands.removeAttribute("right-hand");
+ } else {
+ this._commands.setAttribute("right-hand", true);
+ this._commands.removeAttribute("left-hand");
+ }
+ }
+
+ let hLeft = (aX - halfWidth - width - kPositionPadding) > kPositionPadding;
+ let hRight = (aX + width + kPositionPadding) < screenWidth;
+ let hCenter = (aX - halfWidth - kPositionPadding) > kPositionPadding;
+
+ let vTop = (aY - height - kPositionPadding) > kPositionPadding;
+ let vCenter = (aY - halfHeight - kPositionPadding) > kPositionPadding &&
+ aY + halfHeight < screenHeight;
+ let vBottom = (aY + height + kPositionPadding) < screenHeight;
+
+ if (leftHand && hLeft && vCenter) {
+ dump('leftHand && hLeft && vCenter\n');
+ aX -= (width + halfWidth);
+ aY -= halfHeight;
+ } else if (!leftHand && hRight && vCenter) {
+ dump('!leftHand && hRight && vCenter\n');
+ aX += kPositionPadding;
+ aY -= halfHeight;
+ } else if (vBottom && hCenter) {
+ dump('vBottom && hCenter\n');
+ aX -= halfWidth;
+ } else if (vTop && hCenter) {
+ dump('vTop && hCenter\n');
+ aX -= halfWidth;
+ aY -= height;
+ } else if (hCenter && vCenter) {
+ dump('hCenter && vCenter\n');
+ aX -= halfWidth;
+ aY -= halfHeight;
+ } else {
+ dump('None, left hand: ' + leftHand + '!\n');
+ }
+ }
+
+ if (aX < 0)
+ aX = 0;
+
+ if (aY < 0)
+ aY = 0;
+
+ this._panel.left = aX;
+ this._panel.top = aY;
+
+ let excessY = (aY + height + kPositionPadding - screenHeight);
+ this._popup.style.maxHeight = (excessY > 0) ? (height - excessY) + "px" : "none";
+
+ let excessX = (aX + width + kPositionPadding - screenWidth);
+ this._popup.style.maxWidth = (excessX > 0) ? (width - excessX) + "px" : "none";
+ },
+
+ handleEvent: function handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "keypress":
+ if (!this._wantTypeBehind) {
+ // Hide the context menu so you can't type behind it.
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ if (aEvent.keyCode != aEvent.DOM_VK_ESCAPE)
+ this.hide();
+ }
+ break;
+ case "mousedown":
+ if (!this._popup.contains(aEvent.target)) {
+ aEvent.stopPropagation();
+ this.hide();
+ }
+ break;
+ }
+ }
+};
diff --git a/browser/metro/base/content/helperui/OfflineApps.js b/browser/metro/base/content/helperui/OfflineApps.js
new file mode 100644
index 000000000000..017542336d22
--- /dev/null
+++ b/browser/metro/base/content/helperui/OfflineApps.js
@@ -0,0 +1,85 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/*
+ * Provide supports for Offline Applications
+ */
+var OfflineApps = {
+ offlineAppRequested: function(aRequest, aTarget) {
+ if (!Services.prefs.getBoolPref("browser.offline-apps.notify"))
+ return;
+
+ let currentURI = Services.io.newURI(aRequest.location, aRequest.charset, null);
+
+ // don't bother showing UI if the user has already made a decision
+ if (Services.perms.testExactPermission(currentURI, "offline-app") != Ci.nsIPermissionManager.UNKNOWN_ACTION)
+ return;
+
+ try {
+ if (Services.prefs.getBoolPref("offline-apps.allow_by_default")) {
+ // all pages can use offline capabilities, no need to ask the user
+ return;
+ }
+ } catch(e) {
+ // this pref isn't set by default, ignore failures
+ }
+
+ let host = currentURI.asciiHost;
+ let notificationID = "offline-app-requested-" + host;
+ let notificationBox = Browser.getNotificationBox(aTarget);
+
+ let notification = notificationBox.getNotificationWithValue(notificationID);
+ let strings = Strings.browser;
+ if (notification) {
+ notification.documents.push(aRequest);
+ } else {
+ let buttons = [{
+ label: strings.GetStringFromName("offlineApps.allow"),
+ accessKey: null,
+ callback: function() {
+ for (let i = 0; i < notification.documents.length; i++)
+ OfflineApps.allowSite(notification.documents[i], aTarget);
+ }
+ },{
+ label: strings.GetStringFromName("offlineApps.never"),
+ accessKey: null,
+ callback: function() {
+ for (let i = 0; i < notification.documents.length; i++)
+ OfflineApps.disallowSite(notification.documents[i]);
+ }
+ },{
+ label: strings.GetStringFromName("offlineApps.notNow"),
+ accessKey: null,
+ callback: function() { /* noop */ }
+ }];
+
+ const priority = notificationBox.PRIORITY_INFO_LOW;
+ let message = strings.formatStringFromName("offlineApps.available2", [host], 1);
+ notification = notificationBox.appendNotification(message, notificationID, "", priority, buttons);
+ notification.documents = [aRequest];
+ }
+ },
+
+ allowSite: function(aRequest, aTarget) {
+ let currentURI = Services.io.newURI(aRequest.location, aRequest.charset, null);
+ Services.perms.add(currentURI, "offline-app", Ci.nsIPermissionManager.ALLOW_ACTION);
+
+ // When a site is enabled while loading, manifest resources will start
+ // fetching immediately. This one time we need to do it ourselves.
+ // The update must be started on the content process.
+ aTarget.messageManager.sendAsyncMessage("Browser:MozApplicationCache:Fetch", aRequest);
+ },
+
+ disallowSite: function(aRequest) {
+ let currentURI = Services.io.newURI(aRequest.location, aRequest.charset, null);
+ Services.perms.add(currentURI, "offline-app", Ci.nsIPermissionManager.DENY_ACTION);
+ },
+
+ receiveMessage: function receiveMessage(aMessage) {
+ if (aMessage.name == "Browser:MozApplicationManifest") {
+ this.offlineAppRequested(aMessage.json, aMessage.target);
+ }
+ }
+};
+
diff --git a/browser/metro/base/content/helperui/SelectHelperUI.js b/browser/metro/base/content/helperui/SelectHelperUI.js
new file mode 100644
index 000000000000..b638b75956bc
--- /dev/null
+++ b/browser/metro/base/content/helperui/SelectHelperUI.js
@@ -0,0 +1,157 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+/**
+ * SelectHelperUI: Provides an interface for making a choice in a list.
+ * Supports simultaneous selection of choices and group headers.
+ */
+var SelectHelperUI = {
+ _list: null,
+
+ get _container() {
+ delete this._container;
+ return this._container = document.getElementById("select-container");
+ },
+
+ get _listbox() {
+ delete this._listbox;
+ return this._listbox = document.getElementById("select-commands");
+ },
+
+ get _menuPopup() {
+ let popup = document.getElementById("select-popup");
+ delete this._menuPopup;
+ return this._menuPopup = new MenuPopup(this._container, popup);
+ },
+
+ show: function selectHelperShow(aList, aTitle, aRect) {
+ if (AnimatedZoom.isZooming()) {
+ FormHelperUI._waitForZoom(this.show.bind(this, aList, aTitle, aRect));
+ return;
+ }
+
+ if (this._list)
+ this.reset();
+
+ this._list = aList;
+
+ // The element label is used as a title to give more context
+ this._container.setAttribute("multiple", aList.multiple ? "true" : "false");
+
+ let firstSelected = null;
+
+ // Using a fragment prevent us to hang on huge list
+ let fragment = document.createDocumentFragment();
+ let choices = aList.choices;
+ for (let i = 0; i < choices.length; i++) {
+ let choice = choices[i];
+ let item = document.createElement("listitem");
+
+ item.setAttribute("class", "option-command listitem-iconic action-button");
+ item.setAttribute("flex", "1");
+ item.setAttribute("crop", "center");
+ item.setAttribute("label", choice.text);
+
+ choice.selected ? item.classList.add("selected")
+ : item.classList.remove("selected");
+
+ choice.disabled ? item.setAttribute("disabled", "true")
+ : item.removeAttribute("disabled");
+ fragment.appendChild(item);
+
+ if (choice.group) {
+ item.classList.add("optgroup");
+ continue;
+ }
+
+ item.optionIndex = choice.optionIndex;
+ item.choiceIndex = i;
+
+ if (choice.inGroup)
+ item.classList.add("in-optgroup");
+
+ if (choice.selected) {
+ item.classList.add("selected");
+ firstSelected = firstSelected || item;
+ }
+ }
+ this._listbox.appendChild(fragment);
+
+ this._container.addEventListener("click", this, false);
+ this._menuPopup.show(this._positionOptions(aRect));
+ this._listbox.ensureElementIsVisible(firstSelected);
+ },
+
+ reset: function selectHelperReset() {
+ this._updateControl();
+ while (this._listbox.hasChildNodes())
+ this._listbox.removeChild(this._listbox.lastChild);
+ this._list = null;
+ },
+
+ hide: function selectHelperHide() {
+ if (!this._list)
+ return;
+
+ this._container.removeEventListener("click", this, false);
+ this._menuPopup.hide();
+ this.reset();
+ },
+
+ _positionOptions: function _positionOptions(aRect) {
+ let browser = Browser.selectedBrowser;
+ let p0 = browser.transformBrowserToClient(aRect.left, aRect.top);
+ let p1 = browser.transformBrowserToClient(aRect.right, aRect.bottom);
+
+ return {
+ forcePosition: true,
+ xPos: p0.x,
+ yPos: p1.y,
+ bottomAligned: false,
+ leftAligned: true
+ };
+ },
+
+ _forEachOption: function _selectHelperForEachOption(aCallback) {
+ let children = this._listbox.childNodes;
+ for (let i = 0; i < children.length; i++) {
+ let item = children[i];
+ if (!item.hasOwnProperty("optionIndex"))
+ continue;
+ aCallback(item, i);
+ }
+ },
+
+ _updateControl: function _selectHelperUpdateControl() {
+ Browser.selectedBrowser.messageManager.sendAsyncMessage("FormAssist:ChoiceChange", { });
+ },
+
+ handleEvent: function selectHelperHandleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "click":
+ let item = aEvent.target;
+ if (item && item.hasOwnProperty("optionIndex")) {
+ if (this._list.multiple) {
+ item.classList.toggle("selected");
+ } else {
+ item.classList.add("selected");
+ }
+ this.onSelect(item.optionIndex, item.classList.contains("selected"));
+ }
+ break;
+ }
+ },
+
+ onSelect: function selectHelperOnSelect(aIndex, aSelected) {
+ Browser.selectedBrowser.messageManager.sendAsyncMessage("FormAssist:ChoiceSelect", {
+ index: aIndex,
+ selected: aSelected
+ });
+ if (!this._list.multiple) {
+ this.hide();
+ }
+ }
+};
diff --git a/browser/metro/base/content/helperui/SelectionHelperUI.js b/browser/metro/base/content/helperui/SelectionHelperUI.js
new file mode 100644
index 000000000000..1064de097505
--- /dev/null
+++ b/browser/metro/base/content/helperui/SelectionHelperUI.js
@@ -0,0 +1,678 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+ /*
+ * Markers
+ */
+
+ // Y axis scroll distance that will disable this module and cancel selection
+ const kDisableOnScrollDistance = 25;
+
+function MarkerDragger(aMarker) {
+ this._marker = aMarker;
+}
+
+MarkerDragger.prototype = {
+ _selectionHelperUI: null,
+ _marker: null,
+ _shutdown: false,
+ _dragging: false,
+
+ get marker() {
+ return this._marker;
+ },
+
+ set shutdown(aVal) {
+ this._shutdown = aVal;
+ },
+
+ get shutdown() {
+ return this._shutdown;
+ },
+
+ get dragging() {
+ return this._dragging;
+ },
+
+ freeDrag: function freeDrag() {
+ return true;
+ },
+
+ isDraggable: function isDraggable(aTarget, aContent) {
+ return { x: true, y: true };
+ },
+
+ dragStart: function dragStart(aX, aY, aTarget, aScroller) {
+ if (this._shutdown)
+ return false;
+ this._dragging = true;
+ this.marker.dragStart(aX, aY);
+ return true;
+ },
+
+ dragStop: function dragStop(aDx, aDy, aScroller) {
+ if (this._shutdown)
+ return false;
+ this._dragging = false;
+ this.marker.dragStop(aDx, aDy);
+ return true;
+ },
+
+ dragMove: function dragMove(aDx, aDy, aScroller, aIsKenetic, aClientX, aClientY) {
+ // Note if aIsKenetic is true this is synthetic movement,
+ // we don't want that so return false.
+ if (this._shutdown || aIsKenetic)
+ return false;
+ this.marker.moveBy(aDx, aDy, aClientX, aClientY);
+ // return true if we moved, false otherwise. The result
+ // is used in deciding if we should repaint between drags.
+ return true;
+ }
+}
+
+function Marker() {}
+
+Marker.prototype = {
+ _element: null,
+ _selectionHelperUI: null,
+ _xPos: 0,
+ _yPos: 0,
+ _tag: "",
+ _hPlane: 0,
+ _vPlane: 0,
+
+ // Tweak me if the monocle graphics change in any way
+ _monocleRadius: 8,
+ _monocleXHitTextAdjust: -2,
+ _monocleYHitTextAdjust: -10,
+
+ get xPos() {
+ return this._xPos;
+ },
+
+ get yPos() {
+ return this._yPos;
+ },
+
+ get tag() {
+ return this._tag;
+ },
+
+ set tag(aVal) {
+ this._tag = aVal;
+ },
+
+ get dragging() {
+ return this._element.customDragger.dragging;
+ },
+
+ init: function init(aParent, aElementId, xPos, yPos) {
+ this._selectionHelperUI = aParent;
+ this._element = document.getElementById(aElementId);
+ // These get picked in input.js and receives drag input
+ this._element.customDragger = new MarkerDragger(this);
+ },
+
+ shutdown: function shutdown() {
+ this._element.hidden = true;
+ this._element.customDragger.shutdown = true;
+ delete this._element.customDragger;
+ this._selectionHelperUI = null;
+ this._element = null;
+ },
+
+ setTrackBounds: function setTrackBounds(aVerticalPlane, aHorizontalPlane) {
+ // monocle boundaries
+ this._hPlane = aHorizontalPlane;
+ this._vPlane = aVerticalPlane;
+ },
+
+ show: function show() {
+ this._element.hidden = false;
+ },
+
+ hide: function hide() {
+ this._element.hidden = true;
+ },
+
+ position: function position(aX, aY) {
+ if (aX < 0) {
+ Util.dumpLn("Marker: aX is negative");
+ aX = 0;
+ }
+ if (aY < 0) {
+ Util.dumpLn("Marker: aY is negative");
+ aY = 0;
+ }
+ this._xPos = aX;
+ this._yPos = aY;
+ this._setPosition();
+ },
+
+ _setPosition: function _setPosition() {
+ this._element.left = this._xPos + "px";
+ this._element.top = this._yPos + "px";
+ },
+
+ dragStart: function dragStart(aX, aY) {
+ this._selectionHelperUI.markerDragStart(this);
+ },
+
+ dragStop: function dragStop(aDx, aDy) {
+ this._selectionHelperUI.markerDragStop(this);
+ },
+
+ moveBy: function moveBy(aDx, aDy, aClientX, aClientY) {
+ this._xPos -= aDx;
+ this._yPos -= aDy;
+ this._selectionHelperUI.markerDragMove(this);
+ this._setPosition();
+ },
+
+ hitTest: function hitTest(aX, aY) {
+ // Gets the pointer of the arrow right in the middle of the
+ // monocle.
+ aY += this._monocleYHitTextAdjust;
+ aX += this._monocleXHitTextAdjust;
+ if (aX >= (this._xPos - this._monocleRadius) &&
+ aX <= (this._xPos + this._monocleRadius) &&
+ aY >= (this._yPos - this._monocleRadius) &&
+ aY <= (this._yPos + this._monocleRadius))
+ return true;
+ return false;
+ },
+};
+
+/*
+ * SelectionHelperUI
+ */
+
+var SelectionHelperUI = {
+ _debugEvents: false,
+ _popupState: null,
+ _startMark: null,
+ _endMark: null,
+ _target: null,
+ _movement: { active: false, x:0, y: 0 },
+ _activeSelectionRect: null,
+
+ get startMark() {
+ if (this._startMark == null) {
+ this._startMark = new Marker();
+ this._startMark.tag = "start";
+ this._startMark.init(this, "selectionhandle-start");
+ }
+ return this._startMark;
+ },
+
+ get endMark() {
+ if (this._endMark == null) {
+ this._endMark = new Marker();
+ this._endMark.tag = "end";
+ this._endMark.init(this, "selectionhandle-end");
+ }
+ return this._endMark;
+ },
+
+ get overlay() {
+ return document.getElementById("selection-overlay");
+ },
+
+ /*
+ * openEditSession
+ *
+ * Attempts to select underlying text at a point and begins editing
+ * the section.
+ */
+ openEditSession: function openEditSession(aMessage) {
+ if (!this.canHandle(aMessage))
+ return;
+
+ /*
+ * aMessage - from _onContentContextMenu in ContextMenuHandler
+ * name: aMessage.name,
+ * target: aMessage.target
+ * json:
+ * types: [],
+ * label: "",
+ * linkURL: "",
+ * linkTitle: "",
+ * linkProtocol: null,
+ * mediaURL: "",
+ * xPos: aEvent.x,
+ * yPos: aEvent.y
+ */
+
+ this._popupState = aMessage.json;
+ this._popupState._target = aMessage.target;
+
+ this._init();
+
+ Util.dumpLn("enableSelection target:", this._popupState._target);
+
+ // Set the track bounds for each marker NIY
+ this.startMark.setTrackBounds(this._popupState.xPos, this._popupState.yPos);
+ this.endMark.setTrackBounds(this._popupState.xPos, this._popupState.yPos);
+
+ // Send this over to SelectionHandler in content, they'll message us
+ // back with information on the current selection.
+ this._popupState._target.messageManager.sendAsyncMessage(
+ "Browser:SelectionStart",
+ { xPos: this._popupState.xPos,
+ yPos: this._popupState.yPos });
+
+ this._setupDebugOptions();
+ },
+
+ /*
+ * canHandle
+ *
+ * Determines if we can handle a ContextMenuHandler message.
+ */
+ canHandle: function canHandle(aMessage) {
+ // Reject empty text inputs, nothing to do here
+ if (aMessage.json.types.indexOf("input-empty") != -1) {
+ return false;
+ }
+ // Input with text or general text in a page
+ if (aMessage.json.types.indexOf("content-text") == -1 &&
+ aMessage.json.types.indexOf("input-text") == -1) {
+ return false;
+ }
+ return true;
+ },
+
+ /*
+ * isActive
+ *
+ * Determines if an edit session is currently active.
+ */
+ isActive: function isActive() {
+ return (this._popupState && this._popupState._target);
+ },
+
+ /*
+ * closeEditSession
+ *
+ * Closes an active edit session and shuts down. Does not clear existing
+ * selection regions if they exist.
+ */
+ closeEditSession: function closeEditSession() {
+ if (this._popupState._target)
+ this._popupState._target.messageManager.sendAsyncMessage("Browser:SelectionClose");
+ this._shutdown();
+ },
+
+ /*
+ * closeEditSessionAndClear
+ *
+ * Closes an active edit session and shuts down. Clears any selection region
+ * associated with the edit session.
+ */
+ closeEditSessionAndClear: function closeEditSessionAndClear() {
+ if (this._popupState._target)
+ this._popupState._target.messageManager.sendAsyncMessage("Browser:SelectionClear");
+ this.closeEditSession();
+ },
+
+ /*
+ * Internal
+ */
+
+ _init: function _init() {
+ // SelectionHandler messages
+ messageManager.addMessageListener("Content:SelectionRange", this);
+ messageManager.addMessageListener("Content:SelectionCopied", this);
+ messageManager.addMessageListener("Content:SelectionFail", this);
+ messageManager.addMessageListener("Content:SelectionDebugRect", this);
+
+ // selection related events
+ window.addEventListener("click", this, true);
+ window.addEventListener("dblclick", this, true);
+ window.addEventListener("MozPressTapGesture", this, true);
+
+ // Picking up scroll attempts
+ window.addEventListener("touchstart", this, true);
+ window.addEventListener("touchend", this, true);
+ window.addEventListener("touchmove", this, true);
+
+ // cancellation related events
+ window.addEventListener("keypress", this, true);
+ Elements.browsers.addEventListener("URLChanged", this, true);
+ Elements.browsers.addEventListener("SizeChanged", this, true);
+ Elements.browsers.addEventListener("ZoomChanged", this, true);
+
+ window.addEventListener("MozPrecisePointer", this, true);
+
+ this.overlay.enabled = true;
+ },
+
+ _shutdown: function _shutdown() {
+ messageManager.removeMessageListener("Content:SelectionRange", this);
+ messageManager.removeMessageListener("Content:SelectionCopied", this);
+ messageManager.removeMessageListener("Content:SelectionFail", this);
+ messageManager.removeMessageListener("Content:SelectionDebugRect", this);
+
+ window.removeEventListener("click", this, true);
+ window.removeEventListener("dblclick", this, true);
+ window.removeEventListener("MozPressTapGesture", this, true);
+
+ window.removeEventListener("touchstart", this, true);
+ window.removeEventListener("touchend", this, true);
+ window.removeEventListener("touchmove", this, true);
+
+ window.removeEventListener("keypress", this, true);
+ Elements.browsers.removeEventListener("URLChanged", this, true);
+ Elements.browsers.removeEventListener("SizeChanged", this, true);
+ Elements.browsers.removeEventListener("ZoomChanged", this, true);
+
+ window.removeEventListener("MozPrecisePointer", this, true);
+
+ if (this.startMark != null)
+ this.startMark.shutdown();
+ if (this.endMark != null)
+ this.endMark.shutdown();
+
+ delete this._startMark;
+ delete this._endMark;
+
+ this._popupState = null;
+ this._activeSelectionRect = null;
+
+ this.overlay.displayDebugLayer = false;
+ this.overlay.enabled = false;
+ },
+
+ /*
+ * _setupDebugOptions
+ *
+ * Sends a message over to content instructing it to
+ * turn on various debug features.
+ */
+ _setupDebugOptions: function _setupDebugOptions() {
+ // Debug options for selection
+ let debugOpts = { dumpRanges: false, displayRanges: false, dumpEvents: false };
+ try {
+ if (Services.prefs.getBoolPref(kDebugSelectionDumpPref))
+ debugOpts.displayRanges = true;
+ } catch (ex) {}
+ try {
+ if (Services.prefs.getBoolPref(kDebugSelectionDisplayPref))
+ debugOpts.displayRanges = true;
+ } catch (ex) {}
+ try {
+ if (Services.prefs.getBoolPref(kDebugSelectionDumpEvents)) {
+ debugOpts.dumpEvents = true;
+ this._debugEvents = true;
+ }
+ } catch (ex) {}
+
+ if (debugOpts.displayRanges || debugOpts.dumpRanges || debugOpts.dumpEvents) {
+ // Turn on the debug layer
+ this.overlay.displayDebugLayer = true;
+ // Tell SelectionHandler what to do
+ this._popupState
+ ._target
+ .messageManager
+ .sendAsyncMessage("Browser:SelectionDebug", debugOpts);
+ }
+ },
+
+ /*
+ * Event handlers for document events
+ */
+
+ _checkForActiveDrag: function _checkForActiveDrag() {
+ return (this.startMark.dragging || this.endMark.dragging);
+ },
+
+ _hitTestSelection: function _hitTestSelection(aEvent) {
+ let clickPos =
+ this._popupState._target.transformClientToBrowser(aEvent.clientX,
+ aEvent.clientY);
+ // Ignore if the double tap isn't on our active selection rect.
+ if (this._activeSelectionRect &&
+ Util.pointWithinRect(clickPos.x, clickPos.y, this._activeSelectionRect)) {
+ return true;
+ }
+ return false;
+ },
+
+ _onTap: function _onTap(aEvent) {
+ // Trap single clicks which if forwarded to content will clear selection.
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+
+ if (this.startMark.hitTest(aEvent.clientX, aEvent.clientY) ||
+ this.endMark.hitTest(aEvent.clientX, aEvent.clientY)) {
+ // NIY
+ this._popupState._target.messageManager.sendAsyncMessage("Browser:ChangeMode", {});
+ }
+ },
+
+ _onLongTap: function _onLongTap(aEvent) {
+ // Check to see if this tap is on one of our monocles. Tap is
+ // easy to trigger when dragging them around.
+ if (this.startMark.hitTest(aEvent.clientX, aEvent.clientY) ||
+ this.endMark.hitTest(aEvent.clientX, aEvent.clientY)) {
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ return;
+ }
+ },
+
+ /*
+ * Checks to see if the tap event was on our selection rect.
+ * If it is, we select the underlying text and shutdown.
+ */
+ _onDblTap: function _onDblTap(aEvent) {
+ if (!this._hitTestSelection(aEvent)) {
+ // Clear and close
+ this.closeEditSessionAndClear();
+ return;
+ }
+
+ // Select and close
+ let clickPos =
+ this._popupState._target.transformClientToBrowser(aEvent.clientX,
+ aEvent.clientY);
+ let json = {
+ xPos: clickPos.x,
+ yPos: clickPos.y,
+ };
+
+ this._popupState._target.messageManager.sendAsyncMessage("Browser:SelectionCopy", json);
+
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ },
+
+ _onSelectionCopied: function _onSelectionCopied(json) {
+ if (json.succeeded) {
+ this.showToast(Strings.browser.GetStringFromName("selectionHelper.textCopied"));
+ }
+ this.closeEditSessionAndClear();
+ },
+
+ _onSelectionRangeChange: function _onSelectionRangeChange(json) {
+ if (json.updateStart) {
+ let pos =
+ this._popupState._target.transformBrowserToClient(json.start.xPos, json.start.yPos);
+ this.startMark.position(pos.x, pos.y);
+ this.startMark.show();
+ }
+ if (json.updateEnd) {
+ let pos =
+ this._popupState._target.transformBrowserToClient(json.end.xPos, json.end.yPos);
+ this.endMark.position(pos.x, pos.y);
+ this.endMark.show();
+ }
+ this._activeSelectionRect = json.rect;
+ },
+
+ _onSelectionFail: function _onSelectionFail() {
+ Util.dumpLn("failed to get a selection.");
+ this.closeEditSession();
+ },
+
+ _onKeypress: function _onKeypress() {
+ this.closeEditSession();
+ },
+
+ _onResize: function _onResize() {
+ this._popupState._target
+ .messageManager
+ .sendAsyncMessage("Browser:SelectionUpdate", {});
+ },
+
+ _onDebugRectRequest: function _onDebugRectRequest(aMsg) {
+ this.overlay.addDebugRect(aMsg.left, aMsg.top, aMsg.right, aMsg.bottom,
+ aMsg.color, aMsg.fill, aMsg.id);
+ },
+
+ /*
+ * Events
+ */
+
+ _initMouseEventFromEvent: function _initMouseEventFromEvent(aDestEvent, aSrcEvent, aType) {
+ event.initNSMouseEvent(aType, true, true, content, 0,
+ aSrcEvent.screenX, aSrcEvent.screenY, aSrcEvent.clientX, aSrcEvent.clientY,
+ false, false, false, false,
+ aSrcEvent.button, aSrcEvent.relatedTarget, 1.0,
+ Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH);
+ },
+
+ handleEvent: function handleEvent(aEvent) {
+ if (this._debugEvents && aEvent.type != "touchmove") {
+ Util.dumpLn("SelectionHelperUI:", aEvent.type);
+ }
+ switch (aEvent.type) {
+ case "click":
+ this._onTap(aEvent);
+ break;
+
+ case "dblclick":
+ this._onDblTap(aEvent);
+ break;
+
+ case "MozPressTapGesture":
+ this._onLongTap(aEvent);
+ break;
+
+ case "touchstart": {
+ if (aEvent.touches.length != 1)
+ break;
+ let touch = aEvent.touches[0];
+ this._movement.x = this._movement.y = 0;
+ this._movement.x = touch.clientX;
+ this._movement.y = touch.clientY;
+ this._movement.active = true;
+ break;
+ }
+
+ case "touchend":
+ if (aEvent.touches.length == 0)
+ this._movement.active = false;
+ break;
+
+ case "touchmove": {
+ if (aEvent.touches.length != 1)
+ break;
+ let touch = aEvent.touches[0];
+ // Clear our selection overlay when the user starts to pan the page
+ if (!this._checkForActiveDrag() && this._movement.active) {
+ let distanceY = touch.clientY - this._movement.y;
+ if (Math.abs(distanceY) > kDisableOnScrollDistance) {
+ this.closeEditSessionAndClear();
+ }
+ }
+ break;
+ }
+
+ case "keypress":
+ this._onKeypress(aEvent);
+ break;
+
+ case "SizeChanged":
+ this._onResize(aEvent);
+ break;
+
+ case "ZoomChanged":
+ case "URLChanged":
+ case "MozPrecisePointer":
+ this.closeEditSessionAndClear();
+ break;
+ }
+ },
+
+ receiveMessage: function sh_receiveMessage(aMessage) {
+ if (this._debugEvents) Util.dumpLn("SelectionHelperUI:", aMessage.name);
+ let json = aMessage.json;
+ switch (aMessage.name) {
+ case "Content:SelectionFail":
+ this._onSelectionFail();
+ break;
+ case "Content:SelectionRange":
+ this._onSelectionRangeChange(json);
+ break;
+ case "Content:SelectionCopied":
+ this._onSelectionCopied(json);
+ break;
+ case "Content:SelectionDebugRect":
+ this._onDebugRectRequest(json);
+ break;
+ }
+ },
+
+ /*
+ * Callbacks from markers
+ */
+
+ _getMarkerBaseMessage: function _getMarkerBaseMessage() {
+ /*
+ This appears to be adjusted for scroll and scale. It should only
+ adjust for scale, content handles scroll offsets.
+ let startPos =
+ this._popupState._target.transformBrowserToClient(this.startMark.xPos,
+ this.startMark.yPos);
+ let endPos =
+ this._popupState._target.transformBrowserToClient(this.endMark.xPos,
+ this.endMark.yPos);
+ return {
+ start: { xPos: startPos.x, yPos: startPos.y },
+ end: { xPos: endPos.x, yPos: endPos.y },
+ };
+ */
+ return {
+ start: { xPos: this.startMark.xPos, yPos: this.startMark.yPos },
+ end: { xPos: this.endMark.xPos, yPos: this.endMark.yPos },
+ };
+ },
+
+ markerDragStart: function markerDragStart(aMarker) {
+ let json = this._getMarkerBaseMessage();
+ json.change = aMarker.tag;
+ this._popupState._target.messageManager.sendAsyncMessage("Browser:SelectionMoveStart", json);
+ },
+
+ markerDragStop: function markerDragStop(aMarker) {
+ //aMarker.show();
+ let json = this._getMarkerBaseMessage();
+ json.change = aMarker.tag;
+ this._popupState._target.messageManager.sendAsyncMessage("Browser:SelectionMoveEnd", json);
+ },
+
+ markerDragMove: function markerDragMove(aMarker) {
+ let json = this._getMarkerBaseMessage();
+ json.change = aMarker.tag;
+ this._popupState._target.messageManager.sendAsyncMessage("Browser:SelectionMove", json);
+ },
+
+ showToast: function showToast(aString) {
+ let toaster =
+ Cc["@mozilla.org/toaster-alerts-service;1"]
+ .getService(Ci.nsIAlertsService);
+ toaster.showAlertNotification(null, aString, "", false, "", null);
+ },
+};
diff --git a/browser/metro/base/content/helperui/SharingUI.js b/browser/metro/base/content/helperui/SharingUI.js
new file mode 100644
index 000000000000..b8d0decaf380
--- /dev/null
+++ b/browser/metro/base/content/helperui/SharingUI.js
@@ -0,0 +1,83 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var SharingUI = {
+ _dialog: null,
+
+ show: function show(aURL, aTitle) {
+ try {
+ this.showSharingUI(aURL, aTitle);
+ } catch (ex) {
+ this.showFallback(aURL, aTitle);
+ }
+ },
+
+ showSharingUI: function showSharingUI(aURL, aTitle) {
+ let sharingSvc = Cc["@mozilla.org/uriloader/external-sharing-app-service;1"].getService(Ci.nsIExternalSharingAppService);
+ sharingSvc.shareWithDefault(aURL, "text/plain", aTitle);
+ },
+
+ showFallback: function showFallback(aURL, aTitle) {
+ this._dialog = DialogUI.importModal(window, "chrome://browser/content/prompt/share.xul", null);
+ document.getElementById("share-title").value = aTitle || aURL;
+
+ DialogUI.pushPopup(this, this._dialog);
+
+ let bbox = document.getElementById("share-buttons-box");
+ this._handlers.forEach(function(handler) {
+ let button = document.createElement("button");
+ button.className = "action-button";
+ button.setAttribute("label", handler.name);
+ button.addEventListener("command", function() {
+ SharingUI.hide();
+ handler.callback(aURL || "", aTitle || "");
+ }, false);
+ bbox.appendChild(button);
+ });
+
+ this._dialog.waitForClose();
+ DialogUI.popPopup(this);
+ },
+
+ hide: function hide() {
+ this._dialog.close();
+ this._dialog = null;
+ },
+
+ _handlers: [
+ {
+ name: "Email",
+ callback: function callback(aURL, aTitle) {
+ let url = "mailto:?subject=" + encodeURIComponent(aTitle) +
+ "&body=" + encodeURIComponent(aURL);
+ let uri = Services.io.newURI(url, null, null);
+ let extProtocolSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"]
+ .getService(Ci.nsIExternalProtocolService);
+ extProtocolSvc.loadUrl(uri);
+ }
+ },
+ {
+ name: "Twitter",
+ callback: function callback(aURL, aTitle) {
+ let url = "http://twitter.com/home?status=" + encodeURIComponent((aTitle ? aTitle+": " : "")+aURL);
+ BrowserUI.newTab(url, Browser.selectedTab);
+ }
+ },
+ {
+ name: "Google Reader",
+ callback: function callback(aURL, aTitle) {
+ let url = "http://www.google.com/reader/link?url=" + encodeURIComponent(aURL) +
+ "&title=" + encodeURIComponent(aTitle);
+ BrowserUI.newTab(url, Browser.selectedTab);
+ }
+ },
+ {
+ name: "Facebook",
+ callback: function callback(aURL, aTitle) {
+ let url = "http://www.facebook.com/share.php?u=" + encodeURIComponent(aURL);
+ BrowserUI.newTab(url, Browser.selectedTab);
+ }
+ }
+ ]
+};
diff --git a/browser/metro/base/content/history.js b/browser/metro/base/content/history.js
new file mode 100644
index 000000000000..6c301965e075
--- /dev/null
+++ b/browser/metro/base/content/history.js
@@ -0,0 +1,90 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+'use strict';
+
+
+// TODO : Observe changes in history with nsINavHistoryObserver
+function HistoryView(aSet) {
+ this._set = aSet;
+ this._set.controller = this;
+
+ //add observers here
+}
+
+HistoryView.prototype = {
+ _set:null,
+
+ handleItemClick: function tabview_handleItemClick(aItem) {
+ let url = aItem.getAttribute("value");
+ BrowserUI.goToURI(url);
+ },
+
+ populateGrid: function populateGrid() {
+ let query = gHistSvc.getNewQuery();
+ let options = gHistSvc.getNewQueryOptions();
+ options.excludeQueries = true;
+ options.queryType = options.QUERY_TYPE_HISTORY;
+ options.maxResults = StartUI.maxResultsPerSection;
+ options.resultType = options.RESULTS_AS_URI;
+ options.sortingMode = options.SORT_BY_DATE_DESCENDING;
+
+ let result = gHistSvc.executeQuery(query, options);
+ let rootNode = result.root;
+ rootNode.containerOpen = true;
+ let childCount = rootNode.childCount;
+
+ for (let i = 0; i < childCount; i++) {
+ let node = rootNode.getChild(i);
+ let uri = node.uri;
+ let title = node.title || uri;
+
+ let item = this._set.appendItem(title, uri);
+ item.setAttribute("iconURI", node.icon);
+ }
+
+ rootNode.containerOpen = false;
+ },
+
+ destruct: function destruct() {
+ },
+};
+
+let HistoryStartView = {
+ _view: null,
+ get _grid() { return document.getElementById("start-history-grid"); },
+
+ show: function show() {
+ this._grid.arrangeItems();
+ },
+
+ init: function init() {
+ this._view = new HistoryView(this._grid);
+ this._view.populateGrid();
+ },
+
+ uninit: function uninit() {
+ this._view.destruct();
+ }
+};
+
+let HistoryPanelView = {
+ _view: null,
+ get _grid() { return document.getElementById("history-list"); },
+ get visible() { return PanelUI.isPaneVisible("history-container"); },
+
+ show: function show() {
+ this._grid.arrangeItems();
+ },
+
+ init: function init() {
+ this._view = new HistoryView(this._grid);
+ this._view.populateGrid();
+ },
+
+ uninit: function uninit() {
+ this._view.destruct();
+ }
+};
+
diff --git a/browser/metro/base/content/input.js b/browser/metro/base/content/input.js
new file mode 100644
index 000000000000..1c587b7324e4
--- /dev/null
+++ b/browser/metro/base/content/input.js
@@ -0,0 +1,1212 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; js2-strict-trailing-comma-warning: nil -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+Components.utils.import("resource://gre/modules/Geometry.jsm");
+
+/*
+ * Drag scrolling related constants
+ */
+
+// maximum drag distance in inches while axis locking can still be reverted
+const kAxisLockRevertThreshold = 0.8;
+
+// Same as NS_EVENT_STATE_ACTIVE from nsIEventStateManager.h
+const kStateActive = 0x00000001;
+
+// After a drag begins, kinetic panning is stopped if the drag doesn't become
+// a pan in 300 milliseconds.
+const kStopKineticPanOnDragTimeout = 300;
+
+// Min/max velocity of kinetic panning. This is in pixels/millisecond.
+const kMinVelocity = 0.4;
+const kMaxVelocity = 6;
+
+/*
+ * prefs
+ */
+
+// A debug pref that when set makes us treat all precise pointer input
+// as imprecise touch input. For debugging purposes only. Note there are
+// subtle event sequencing differences in this feature when running on
+// the desktop using the win32 widget backend and the winrt widget backend
+// in metro. Fixing something in this mode does not insure the bug is
+// in metro.
+const kDebugMouseInputPref = "metro.debug.treatmouseastouch";
+// Colorizes the touch input overlay so you know when it is active.
+const kDebugMouseLayerPref = "metro.debug.colorizeInputOverlay";
+// Display rects around selection ranges. Useful in debugging
+// selection problems.
+const kDebugSelectionDisplayPref = "metro.debug.selection.displayRanges";
+// Dump range rect data to the console. Very useful, but also slows
+// things down a lot.
+const kDebugSelectionDumpPref = "metro.debug.selection.dumpRanges";
+// Dump message manager event traffic for selection.
+const kDebugSelectionDumpEvents = "metro.debug.selection.dumpEvents";
+
+/**
+ * TouchModule
+ *
+ * Handles all touch-related input such as dragging and tapping.
+ *
+ * The Fennec chrome DOM tree has elements that are augmented dynamically with
+ * custom JS properties that tell the TouchModule they have custom support for
+ * either dragging or clicking. These JS properties are JS objects that expose
+ * an interface supporting dragging or clicking (though currently we only look
+ * to drag scrollable elements).
+ *
+ * A custom dragger is a JS property that lives on a scrollable DOM element,
+ * accessible as myElement.customDragger. The customDragger must support the
+ * following interface: (The `scroller' argument is given for convenience, and
+ * is the object reference to the element's scrollbox object).
+ *
+ * dragStart(cX, cY, target, scroller)
+ * Signals the beginning of a drag. Coordinates are passed as
+ * client coordinates. target is copied from the event.
+ *
+ * dragStop(dx, dy, scroller)
+ * Signals the end of a drag. The dx, dy parameters may be non-zero to
+ * indicate one last drag movement.
+ *
+ * dragMove(dx, dy, scroller, isKinetic)
+ * Signals an input attempt to drag by dx, dy.
+ *
+ * There is a default dragger in case a scrollable element is dragged --- see
+ * the defaultDragger prototype property.
+ */
+
+var TouchModule = {
+ _debugEvents: false,
+ _isCancelled: false,
+ _isCancellable: false,
+
+ init: function init() {
+ this._dragData = new DragData();
+
+ this._dragger = null;
+
+ this._targetScrollbox = null;
+ this._targetScrollInterface = null;
+
+ this._kinetic = new KineticController(this._dragBy.bind(this),
+ this._kineticStop.bind(this));
+
+ // capture phase events
+ window.addEventListener("CancelTouchSequence", this, true);
+
+ // bubble phase
+ window.addEventListener("contextmenu", this, false);
+ window.addEventListener("touchstart", this, false);
+ window.addEventListener("touchmove", this, false);
+ window.addEventListener("touchend", this, false);
+
+ try {
+ this._treatMouseAsTouch = Services.prefs.getBoolPref(kDebugMouseInputPref);
+ } catch (e) {}
+ },
+
+ /*
+ * Mouse input source tracking
+ */
+
+ _treatMouseAsTouch: false,
+
+ /*
+ * Events
+ */
+
+ handleEvent: function handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "contextmenu":
+ this._onContextMenu(aEvent);
+ break;
+
+ case "CancelTouchSequence":
+ this.cancelPending();
+ break;
+
+ default: {
+ if (this._debugEvents) {
+ if (aEvent.type != "touchmove")
+ Util.dumpLn("TouchModule:", aEvent.type, aEvent.target);
+ }
+
+ switch (aEvent.type) {
+ case "touchstart":
+ this._onTouchStart(aEvent);
+ break;
+ case "touchmove":
+ this._onTouchMove(aEvent);
+ break;
+ case "touchend":
+ this._onTouchEnd(aEvent);
+ break;
+ }
+ }
+ }
+ },
+
+ sample: function sample(aTimeStamp) {
+ this._waitingForPaint = false;
+ },
+
+ /**
+ * This gets invoked by the input handler if another module grabs. We should
+ * reset our state or something here. This is probably doing the wrong thing
+ * in its current form.
+ */
+ cancelPending: function cancelPending() {
+ this._doDragStop();
+
+ // Kinetic panning may have already been active or drag stop above may have
+ // made kinetic panning active.
+ this._kinetic.end();
+
+ this._targetScrollbox = null;
+ this._targetScrollInterface = null;
+
+ this._cleanClickBuffer();
+ },
+
+ _onContextMenu: function _onContextMenu(aEvent) {
+ // Special case when running on the desktop, fire off
+ // a edge ui event when we get the contextmenu event.
+ if (this._treatMouseAsTouch) {
+ let event = document.createEvent("Events");
+ event.initEvent("MozEdgeUIGesture", true, false);
+ window.dispatchEvent(event);
+ return;
+ }
+
+ // bug 598965 - chrome UI should stop to be pannable once the
+ // context menu has appeared.
+ if (ContextMenuUI.popupState) {
+ this.cancelPending();
+ }
+ },
+
+ /** Begin possible pan and send tap down event. */
+ _onTouchStart: function _onTouchStart(aEvent) {
+ if (aEvent.touches.length > 1)
+ return;
+
+ this._isCancelled = false;
+ this._isCancellable = true;
+
+ if (aEvent.defaultPrevented) {
+ this._isCancelled = true;
+ return;
+ }
+
+ let dragData = this._dragData;
+ if (dragData.dragging) {
+ // Somehow a mouse up was missed.
+ this._doDragStop();
+ }
+ dragData.reset();
+ this.dX = 0;
+ this.dY = 0;
+
+ // walk up the DOM tree in search of nearest scrollable ancestor. nulls are
+ // returned if none found.
+ let [targetScrollbox, targetScrollInterface, dragger]
+ = ScrollUtils.getScrollboxFromElement(aEvent.originalTarget);
+
+ // stop kinetic panning if targetScrollbox has changed
+ if (this._kinetic.isActive() && this._dragger != dragger)
+ this._kinetic.end();
+
+ this._targetScrollbox = targetScrollInterface ? targetScrollInterface.element : targetScrollbox;
+ this._targetScrollInterface = targetScrollInterface;
+
+ if (!this._targetScrollbox) {
+ return false;
+ }
+
+ // XXX shouldn't dragger always be valid here?
+ if (dragger) {
+ let draggable = dragger.isDraggable(targetScrollbox, targetScrollInterface);
+ dragData.locked = !draggable.x || !draggable.y;
+ if (draggable.x || draggable.y) {
+ this._dragger = dragger;
+ if (dragger.freeDrag)
+ dragData.alwaysFreeDrag = dragger.freeDrag();
+ this._doDragStart(aEvent, draggable);
+ }
+ }
+ },
+
+ /** Send tap up event and any necessary full taps. */
+ _onTouchEnd: function _onTouchEnd(aEvent) {
+ if (aEvent.touches.length > 0 || this._isCancelled || !this._targetScrollbox)
+ return;
+
+ // onMouseMove will not record the delta change if we are waiting for a
+ // paint. Since this is the last input for this drag, we override the flag.
+ this._waitingForPaint = false;
+ this._onTouchMove(aEvent);
+
+ let dragData = this._dragData;
+ this._doDragStop();
+ },
+
+ /**
+ * If we're in a drag, do what we have to do to drag on.
+ */
+ _onTouchMove: function _onTouchMove(aEvent) {
+ if (aEvent.touches.length > 1)
+ return;
+
+ if (this._isCancellable) {
+ // only the first touchmove is cancellable.
+ this._isCancellable = false;
+ if (aEvent.defaultPrevented)
+ this._isCancelled = true;
+ }
+
+ if (this._isCancelled)
+ return;
+
+ let touch = aEvent.changedTouches[0];
+ if (!this._targetScrollbox) {
+ return;
+ }
+
+ let dragData = this._dragData;
+
+ if (dragData.dragging) {
+ let oldIsPan = dragData.isPan();
+ dragData.setDragPosition(touch.screenX, touch.screenY);
+ dragData.setMousePosition(touch);
+
+ // Kinetic panning is sensitive to time. It is more stable if it receives
+ // the mousemove events as they come. For dragging though, we only want
+ // to call _dragBy if we aren't waiting for a paint (so we don't spam the
+ // main browser loop with a bunch of redundant paints).
+ //
+ // Here, we feed kinetic panning drag differences for mouse events as
+ // come; for dragging, we build up a drag buffer in this.dX/this.dY and
+ // release it when we are ready to paint.
+ //
+ let [sX, sY] = dragData.panPosition();
+ this.dX += dragData.prevPanX - sX;
+ this.dY += dragData.prevPanY - sY;
+
+ if (dragData.isPan()) {
+ // Only pan when mouse event isn't part of a click. Prevent jittering on tap.
+ this._kinetic.addData(sX - dragData.prevPanX, sY - dragData.prevPanY);
+
+ // dragBy will reset dX and dY values to 0
+ this._dragBy(this.dX, this.dY);
+
+ // Let everyone know when mousemove begins a pan
+ if (!oldIsPan && dragData.isPan()) {
+ //this._longClickTimeout.clear();
+
+ let event = document.createEvent("Events");
+ event.initEvent("PanBegin", true, false);
+ this._targetScrollbox.dispatchEvent(event);
+
+ Browser.selectedBrowser.messageManager.sendAsyncMessage("Browser:PanBegin", {});
+ }
+ }
+ }
+ },
+
+ /**
+ * Inform our dragger of a dragStart.
+ */
+ _doDragStart: function _doDragStart(aEvent, aDraggable) {
+ let touch = aEvent.changedTouches[0];
+ let dragData = this._dragData;
+ dragData.setDragStart(touch.screenX, touch.screenY, aDraggable);
+ this._kinetic.addData(0, 0);
+ this._dragStartTime = Date.now();
+ if (!this._kinetic.isActive()) {
+ this._dragger.dragStart(touch.clientX, touch.clientY, touch.target, this._targetScrollInterface);
+ }
+ },
+
+ /** Finish a drag. */
+ _doDragStop: function _doDragStop() {
+ let dragData = this._dragData;
+ if (!dragData.dragging)
+ return;
+
+ dragData.endDrag();
+
+ // Note: it is possible for kinetic scrolling to be active from a
+ // mousedown/mouseup event previous to this one. In this case, we
+ // want the kinetic panner to tell our drag interface to stop.
+
+ if (dragData.isPan()) {
+ if (Date.now() - this._dragStartTime > kStopKineticPanOnDragTimeout)
+ this._kinetic._velocity.set(0, 0);
+ // Start kinetic pan.
+ this._kinetic.start();
+ } else {
+ this._kinetic.end();
+ if (this._dragger)
+ this._dragger.dragStop(0, 0, this._targetScrollInterface);
+ this._dragger = null;
+ }
+ },
+
+ /**
+ * Used by _onTouchMove() above and by KineticController's timer to do the
+ * actual dragMove signalling to the dragger. We'd put this in _onTouchMove()
+ * but then KineticController would be adding to its own data as it signals
+ * the dragger of dragMove()s.
+ */
+ _dragBy: function _dragBy(dX, dY, aIsKinetic) {
+ let dragged = true;
+ let dragData = this._dragData;
+ if (!this._waitingForPaint || aIsKinetic) {
+ let dragData = this._dragData;
+ dragged = this._dragger.dragMove(dX, dY, this._targetScrollInterface, aIsKinetic,
+ dragData._mouseX, dragData._mouseY);
+ if (dragged && !this._waitingForPaint) {
+ this._waitingForPaint = true;
+ mozRequestAnimationFrame(this);
+ }
+ this.dX = 0;
+ this.dY = 0;
+ }
+ if (!dragData.isPan())
+ this._kinetic.pause();
+
+ return dragged;
+ },
+
+ /** Callback for kinetic scroller. */
+ _kineticStop: function _kineticStop() {
+ // Kinetic panning could finish while user is panning, so don't finish
+ // the pan just yet.
+ let dragData = this._dragData;
+ if (!dragData.dragging) {
+ if (this._dragger)
+ this._dragger.dragStop(0, 0, this._targetScrollInterface);
+ this._dragger = null;
+
+ let event = document.createEvent("Events");
+ event.initEvent("PanFinished", true, false);
+ this._targetScrollbox.dispatchEvent(event);
+ }
+ },
+
+ toString: function toString() {
+ return '[TouchModule] {'
+ + '\n\tdragData=' + this._dragData + ', '
+ + 'dragger=' + this._dragger + ', '
+ + '\n\ttargetScroller=' + this._targetScrollInterface + '}';
+ },
+};
+
+var ScrollUtils = {
+ // threshold in pixels for sensing a tap as opposed to a pan
+ get tapRadius() {
+ let dpi = Util.displayDPI;
+ delete this.tapRadius;
+ return this.tapRadius = Services.prefs.getIntPref("ui.dragThresholdX") / 240 * dpi;
+ },
+
+ /**
+ * Walk up (parentward) the DOM tree from elem in search of a scrollable element.
+ * Return the element and its scroll interface if one is found, two nulls otherwise.
+ *
+ * This function will cache the pointer to the scroll interface on the element itself,
+ * so it is safe to call it many times without incurring the same XPConnect overhead
+ * as in the initial call.
+ */
+ getScrollboxFromElement: function getScrollboxFromElement(elem) {
+ let scrollbox = null;
+ let qinterface = null;
+ // if element is content, get the browser scroll interface
+ if (elem.ownerDocument == Browser.selectedBrowser.contentDocument) {
+ elem = Browser.selectedBrowser;
+ }
+ for (; elem; elem = elem.parentNode) {
+ try {
+ if (elem.anonScrollBox) {
+ scrollbox = elem.anonScrollBox;
+ qinterface = scrollbox.boxObject.QueryInterface(Ci.nsIScrollBoxObject);
+ } else if (elem.scrollBoxObject) {
+ scrollbox = elem;
+ qinterface = elem.scrollBoxObject;
+ break;
+ } else if (elem.customDragger) {
+ scrollbox = elem;
+ break;
+ } else if (elem.boxObject) {
+ let qi = (elem._cachedSBO) ? elem._cachedSBO
+ : elem.boxObject.QueryInterface(Ci.nsIScrollBoxObject);
+ if (qi) {
+ scrollbox = elem;
+ scrollbox._cachedSBO = qinterface = qi;
+ break;
+ }
+ }
+ } catch (e) { /* we aren't here to deal with your exceptions, we'll just keep
+ traversing until we find something more well-behaved, as we
+ prefer default behaviour to whiny scrollers. */ }
+ }
+ return [scrollbox, qinterface, (scrollbox ? (scrollbox.customDragger || this._defaultDragger) : null)];
+ },
+
+ /** Determine if the distance moved can be considered a pan */
+ isPan: function isPan(aPoint, aPoint2) {
+ return (Math.abs(aPoint.x - aPoint2.x) > this.tapRadius ||
+ Math.abs(aPoint.y - aPoint2.y) > this.tapRadius);
+ },
+
+ /**
+ * The default dragger object used by TouchModule when dragging a scrollable
+ * element that provides no customDragger. Simply performs the expected
+ * regular scrollBy calls on the scroller.
+ */
+ _defaultDragger: {
+ isDraggable: function isDraggable(target, scroller) {
+ let sX = {}, sY = {},
+ pX = {}, pY = {};
+ scroller.getPosition(pX, pY);
+ scroller.getScrolledSize(sX, sY);
+ let rect = target.getBoundingClientRect();
+ return { x: (sX.value > rect.width || pX.value != 0),
+ y: (sY.value > rect.height || pY.value != 0) };
+ },
+
+ dragStart: function dragStart(cx, cy, target, scroller) {
+ scroller.element.addEventListener("PanBegin", this._showScrollbars, false);
+ },
+
+ dragStop: function dragStop(dx, dy, scroller) {
+ scroller.element.removeEventListener("PanBegin", this._showScrollbars, false);
+ return this.dragMove(dx, dy, scroller);
+ },
+
+ dragMove: function dragMove(dx, dy, scroller) {
+ if (scroller.getPosition) {
+ try {
+ let oldX = {}, oldY = {};
+ scroller.getPosition(oldX, oldY);
+
+ scroller.scrollBy(dx, dy);
+
+ let newX = {}, newY = {};
+ scroller.getPosition(newX, newY);
+
+ return (newX.value != oldX.value) || (newY.value != oldY.value);
+
+ } catch (e) { /* we have no time for whiny scrollers! */ }
+ }
+
+ return false;
+ },
+
+ _showScrollbars: function _showScrollbars(aEvent) {
+ let scrollbox = aEvent.target;
+ scrollbox.setAttribute("panning", "true");
+
+ let hideScrollbars = function() {
+ scrollbox.removeEventListener("PanFinished", hideScrollbars, false);
+ scrollbox.removeEventListener("CancelTouchSequence", hideScrollbars, false);
+ scrollbox.removeAttribute("panning");
+ }
+
+ // Wait for panning to be completely finished before removing scrollbars
+ scrollbox.addEventListener("PanFinished", hideScrollbars, false);
+ scrollbox.addEventListener("CancelTouchSequence", hideScrollbars, false);
+ }
+ }
+};
+
+/**
+ * DragData handles processing drags on the screen, handling both
+ * locking of movement on one axis, and click detection.
+ */
+function DragData() {
+ this._domUtils = Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+ this._lockRevertThreshold = Util.displayDPI * kAxisLockRevertThreshold;
+ this.reset();
+};
+
+DragData.prototype = {
+ reset: function reset() {
+ this.dragging = false;
+ this.sX = null;
+ this.sY = null;
+ this.locked = false;
+ this.stayLocked = false;
+ this.alwaysFreeDrag = false;
+ this.lockedX = null;
+ this.lockedY = null;
+ this._originX = null;
+ this._originY = null;
+ this.prevPanX = null;
+ this.prevPanY = null;
+ this._isPan = false;
+ },
+
+ /** Depending on drag data, locks sX,sY to X-axis or Y-axis of start position. */
+ _lockAxis: function _lockAxis(sX, sY) {
+ if (this.locked) {
+ if (this.lockedX !== null)
+ sX = this.lockedX;
+ else if (this.lockedY !== null)
+ sY = this.lockedY;
+ return [sX, sY];
+ }
+ else {
+ return [this._originX, this._originY];
+ }
+ },
+
+ setMousePosition: function setMousePosition(aEvent) {
+ this._mouseX = aEvent.clientX;
+ this._mouseY = aEvent.clientY;
+ },
+
+ setDragPosition: function setDragPosition(sX, sY) {
+ // Check if drag is now a pan.
+ if (!this._isPan) {
+ this._isPan = ScrollUtils.isPan(new Point(this._originX, this._originY), new Point(sX, sY));
+ if (this._isPan) {
+ this._resetActive();
+ }
+ }
+
+ // If now a pan, mark previous position where panning was.
+ if (this._isPan) {
+ let absX = Math.abs(this._originX - sX);
+ let absY = Math.abs(this._originY - sY);
+
+ if (absX > this._lockRevertThreshold || absY > this._lockRevertThreshold)
+ this.stayLocked = true;
+
+ // After the first lock, see if locking decision should be reverted.
+ if (!this.stayLocked) {
+ if (this.lockedX && absX > 3 * absY)
+ this.lockedX = null;
+ else if (this.lockedY && absY > 3 * absX)
+ this.lockedY = null;
+ }
+
+ if (!this.locked) {
+ // look at difference from origin coord to lock movement, but only
+ // do it if initial movement is sufficient to detect intent
+
+ // divide possibilty space into eight parts. Diagonals will allow
+ // free movement, while moving towards a cardinal will lock that
+ // axis. We pick a direction if you move more than twice as far
+ // on one axis than another, which should be an angle of about 30
+ // degrees from the axis
+
+ if (absX > 2.5 * absY)
+ this.lockedY = sY;
+ else if (absY > absX)
+ this.lockedX = sX;
+
+ this.locked = true;
+ }
+ }
+
+ // Never lock if the dragger requests it
+ if (this.alwaysFreeDrag) {
+ this.lockedY = null;
+ this.lockedX = null;
+ }
+
+ // After pan lock, figure out previous panning position. Base it on last drag
+ // position so there isn't a jump in panning.
+ let [prevX, prevY] = this._lockAxis(this.sX, this.sY);
+ this.prevPanX = prevX;
+ this.prevPanY = prevY;
+
+ this.sX = sX;
+ this.sY = sY;
+ },
+
+ setDragStart: function setDragStart(screenX, screenY, aDraggable) {
+ this.sX = this._originX = screenX;
+ this.sY = this._originY = screenY;
+ this.dragging = true;
+
+ // If the target area is pannable only in one direction lock it early
+ // on the right axis
+ this.lockedX = !aDraggable.x ? screenX : null;
+ this.lockedY = !aDraggable.y ? screenY : null;
+ this.stayLocked = this.lockedX || this.lockedY;
+ this.locked = this.stayLocked;
+ },
+
+ endDrag: function endDrag() {
+ this._resetActive();
+ this.dragging = false;
+ },
+
+ /** Returns true if drag should pan scrollboxes.*/
+ isPan: function isPan() {
+ return this._isPan;
+ },
+
+ /** Return true if drag should be parsed as a click. */
+ isClick: function isClick() {
+ return !this._isPan;
+ },
+
+ /**
+ * Returns the screen position for a pan. This factors in axis locking.
+ * @return Array of screen X and Y coordinates
+ */
+ panPosition: function panPosition() {
+ return this._lockAxis(this.sX, this.sY);
+ },
+
+ /** dismiss the active state of the pan element */
+ _resetActive: function _resetActive() {
+ let target = document.documentElement;
+ // If the target is active, toggle (turn off) the active flag. Otherwise do nothing.
+ if (this._domUtils.getContentState(target) & kStateActive)
+ this._domUtils.setContentState(target, kStateActive);
+ },
+
+ toString: function toString() {
+ return '[DragData] { sX,sY=' + this.sX + ',' + this.sY + ', dragging=' + this.dragging + ' }';
+ }
+};
+
+
+/**
+ * KineticController - a class to take drag position data and use it
+ * to do kinetic panning of a scrollable object.
+ *
+ * aPanBy is a function that will be called with the dx and dy
+ * generated by the kinetic algorithm. It should return true if the
+ * object was panned, false if there was no movement.
+ *
+ * There are two complicated things done here. One is calculating the
+ * initial velocity of the movement based on user input. Two is
+ * calculating the distance to move every frame.
+ */
+function KineticController(aPanBy, aEndCallback) {
+ this._panBy = aPanBy;
+ this._beforeEnd = aEndCallback;
+
+ // These are used to calculate the position of the scroll panes during kinetic panning. Think of
+ // these points as vectors that are added together and multiplied by scalars.
+ this._position = new Point(0, 0);
+ this._velocity = new Point(0, 0);
+ this._acceleration = new Point(0, 0);
+ this._time = 0;
+ this._timeStart = 0;
+
+ // How often do we change the position of the scroll pane? Too often and panning may jerk near
+ // the end. Too little and panning will be choppy. In milliseconds.
+ this._updateInterval = Services.prefs.getIntPref("browser.ui.kinetic.updateInterval");
+ // Constants that affect the "friction" of the scroll pane.
+ this._exponentialC = Services.prefs.getIntPref("browser.ui.kinetic.exponentialC");
+ this._polynomialC = Services.prefs.getIntPref("browser.ui.kinetic.polynomialC") / 1000000;
+ // Number of milliseconds that can contain a swipe. Movements earlier than this are disregarded.
+ this._swipeLength = Services.prefs.getIntPref("browser.ui.kinetic.swipeLength");
+
+ this._reset();
+}
+
+KineticController.prototype = {
+ _reset: function _reset() {
+ this._active = false;
+ this._paused = false;
+ this.momentumBuffer = [];
+ this._velocity.set(0, 0);
+ },
+
+ isActive: function isActive() {
+ return this._active;
+ },
+
+ _startTimer: function _startTimer() {
+ let self = this;
+
+ let lastp = this._position; // track last position vector because pan takes deltas
+ let v0 = this._velocity; // initial velocity
+ let a = this._acceleration; // acceleration
+ let c = this._exponentialC;
+ let p = new Point(0, 0);
+ let dx, dy, t, realt;
+
+ function calcP(v0, a, t) {
+ // Important traits for this function:
+ // p(t=0) is 0
+ // p'(t=0) is v0
+ //
+ // We use exponential to get a smoother stop, but by itself exponential
+ // is too smooth at the end. Adding a polynomial with the appropriate
+ // weight helps to balance
+ return v0 * Math.exp(-t / c) * -c + a * t * t + v0 * c;
+ }
+
+ this._calcV = function(v0, a, t) {
+ return v0 * Math.exp(-t / c) + 2 * a * t;
+ }
+
+ let callback = {
+ sample: function kineticHandleEvent(timeStamp) {
+ // Someone called end() on us between timer intervals
+ // or we are paused.
+ if (!self.isActive() || self._paused)
+ return;
+
+ // To make animation end fast enough but to keep smoothness, average the ideal
+ // time frame (smooth animation) with the actual time lapse (end fast enough).
+ // Animation will never take longer than 2 times the ideal length of time.
+ realt = timeStamp - self._initialTime;
+ self._time += self._updateInterval;
+ t = (self._time + realt) / 2;
+
+ // Calculate new position.
+ p.x = calcP(v0.x, a.x, t);
+ p.y = calcP(v0.y, a.y, t);
+ dx = Math.round(p.x - lastp.x);
+ dy = Math.round(p.y - lastp.y);
+
+ // Test to see if movement is finished for each component.
+ if (dx * a.x > 0) {
+ dx = 0;
+ lastp.x = 0;
+ v0.x = 0;
+ a.x = 0;
+ }
+ // Symmetric to above case.
+ if (dy * a.y > 0) {
+ dy = 0;
+ lastp.y = 0;
+ v0.y = 0;
+ a.y = 0;
+ }
+
+ if (v0.x == 0 && v0.y == 0) {
+ self.end();
+ } else {
+ let panStop = false;
+ if (dx != 0 || dy != 0) {
+ try { panStop = !self._panBy(-dx, -dy, true); } catch (e) {}
+ lastp.add(dx, dy);
+ }
+
+ if (panStop)
+ self.end();
+ else
+ mozRequestAnimationFrame(this);
+ }
+ }
+ };
+
+ this._active = true;
+ this._paused = false;
+ mozRequestAnimationFrame(callback);
+ },
+
+ start: function start() {
+ function sign(x) {
+ return x ? ((x > 0) ? 1 : -1) : 0;
+ }
+
+ function clampFromZero(x, closerToZero, furtherFromZero) {
+ if (x >= 0)
+ return Math.max(closerToZero, Math.min(furtherFromZero, x));
+ return Math.min(-closerToZero, Math.max(-furtherFromZero, x));
+ }
+
+ let mb = this.momentumBuffer;
+ let mblen = this.momentumBuffer.length;
+
+ let lastTime = mb[mblen - 1].t;
+ let distanceX = 0;
+ let distanceY = 0;
+ let swipeLength = this._swipeLength;
+
+ // determine speed based on recorded input
+ let me;
+ for (let i = 0; i < mblen; i++) {
+ me = mb[i];
+ if (lastTime - me.t < swipeLength) {
+ distanceX += me.dx;
+ distanceY += me.dy;
+ }
+ }
+
+ let currentVelocityX = 0;
+ let currentVelocityY = 0;
+
+ if (this.isActive()) {
+ // If active, then we expect this._calcV to be defined.
+ let currentTime = Date.now() - this._initialTime;
+ currentVelocityX = Util.clamp(this._calcV(this._velocity.x, this._acceleration.x, currentTime), -kMaxVelocity, kMaxVelocity);
+ currentVelocityY = Util.clamp(this._calcV(this._velocity.y, this._acceleration.y, currentTime), -kMaxVelocity, kMaxVelocity);
+ }
+
+ if (currentVelocityX * this._velocity.x <= 0)
+ currentVelocityX = 0;
+ if (currentVelocityY * this._velocity.y <= 0)
+ currentVelocityY = 0;
+
+ let swipeTime = Math.min(swipeLength, lastTime - mb[0].t);
+ this._velocity.x = clampFromZero((distanceX / swipeTime) + currentVelocityX, Math.abs(currentVelocityX), kMaxVelocity);
+ this._velocity.y = clampFromZero((distanceY / swipeTime) + currentVelocityY, Math.abs(currentVelocityY), kMaxVelocity);
+
+ if (Math.abs(this._velocity.x) < kMinVelocity)
+ this._velocity.x = 0;
+ if (Math.abs(this._velocity.y) < kMinVelocity)
+ this._velocity.y = 0;
+
+ // Set acceleration vector to opposite signs of velocity
+ this._acceleration.set(this._velocity.clone().map(sign).scale(-this._polynomialC));
+
+ this._position.set(0, 0);
+ this._initialTime = mozAnimationStartTime;
+ this._time = 0;
+ this.momentumBuffer = [];
+
+ if (!this.isActive() || this._paused)
+ this._startTimer();
+
+ return true;
+ },
+
+ pause: function pause() {
+ this._paused = true;
+ },
+
+ end: function end() {
+ if (this.isActive()) {
+ if (this._beforeEnd)
+ this._beforeEnd();
+ this._reset();
+ }
+ },
+
+ addData: function addData(dx, dy) {
+ let mbLength = this.momentumBuffer.length;
+ let now = Date.now();
+
+ if (this.isActive()) {
+ // Stop active movement when dragging in other direction.
+ if (dx * this._velocity.x < 0 || dy * this._velocity.y < 0)
+ this.end();
+ }
+
+ this.momentumBuffer.push({'t': now, 'dx' : dx, 'dy' : dy});
+ }
+};
+
+
+/**
+ * Input module for basic scrollwheel input. Currently just zooms the browser
+ * view accordingly.
+ */
+var ScrollwheelModule = {
+ _pendingEvent : 0,
+ _container: null,
+
+ init: function init(container) {
+ this._container = container;
+ window.addEventListener("MozPrecisePointer", this, true);
+ window.addEventListener("MozImprecisePointer", this, true);
+ },
+
+ handleEvent: function handleEvent(aEvent) {
+ switch(aEvent.type) {
+ case "DOMMouseScroll":
+ case "MozMousePixelScroll":
+ this._onScroll(aEvent);
+ break;
+ case "MozPrecisePointer":
+ this._container.removeEventListener("DOMMouseScroll", this, true);
+ this._container.removeEventListener("MozMousePixelScroll", this, true);
+ break;
+ case "MozImprecisePointer":
+ this._container.addEventListener("DOMMouseScroll", this, true);
+ this._container.addEventListener("MozMousePixelScroll", this, true);
+ break;
+ };
+ },
+
+ _onScroll: function _onScroll(aEvent) {
+ // If events come too fast we don't want their handling to lag the
+ // zoom in/zoom out execution. With the timeout the zoom is executed
+ // as we scroll.
+ if (this._pendingEvent)
+ clearTimeout(this._pendingEvent);
+
+ this._pendingEvent = setTimeout(function handleEventImpl(self) {
+ self._pendingEvent = 0;
+ Browser.zoom(aEvent.detail);
+ }, 0, this);
+
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ },
+
+ /* We don't have much state to reset if we lose event focus */
+ cancelPending: function cancelPending() {}
+};
+
+
+/*
+ * Simple gestures support
+ */
+
+var GestureModule = {
+ _debugEvents: false,
+
+ init: function init() {
+ window.addEventListener("MozSwipeGesture", this, true);
+ /*
+ window.addEventListener("MozMagnifyGestureStart", this, true);
+ window.addEventListener("MozMagnifyGestureUpdate", this, true);
+ window.addEventListener("MozMagnifyGesture", this, true);
+ */
+ window.addEventListener("CancelTouchSequence", this, true);
+ },
+
+ _initMouseEventFromGestureEvent: function _initMouseEventFromGestureEvent(aDestEvent, aSrcEvent, aType, aCanBubble, aCancellable) {
+ aDestEvent.initMouseEvent(aType, aCanBubble, aCancellable, window, null,
+ aSrcEvent.screenX, aSrcEvent.screenY, aSrcEvent.clientX, aSrcEvent.clientY,
+ aSrcEvent.ctrlKey, aSrcEvent.altKey, aSrcEvent.shiftKey, aSrcEvent.metaKey,
+ aSrcEvent.button, aSrcEvent.relatedTarget);
+ },
+
+ /*
+ * Events
+ *
+ * Dispatch events based on the type of mouse gesture event. For now, make
+ * sure to stop propagation of every gesture event so that web content cannot
+ * receive gesture events.
+ *
+ * @param nsIDOMEvent information structure
+ */
+
+ handleEvent: function handleEvent(aEvent) {
+ try {
+ aEvent.stopPropagation();
+ aEvent.preventDefault();
+ if (this._debugEvents) Util.dumpLn("GestureModule:", aEvent.type);
+ switch (aEvent.type) {
+ case "MozSwipeGesture":
+ if (this._onSwipe(aEvent)) {
+ let event = document.createEvent("Events");
+ event.initEvent("CancelTouchSequence", true, true);
+ aEvent.target.dispatchEvent(event);
+ }
+ break;
+
+ // Magnify currently doesn't work for Win8 (bug 593168)
+ /*
+ case "MozMagnifyGestureStart":
+ this._pinchStart(aEvent);
+ break;
+
+ case "MozMagnifyGestureUpdate":
+ this._pinchUpdate(aEvent);
+ break;
+
+ case "MozMagnifyGesture":
+ this._pinchEnd(aEvent);
+ break;
+ */
+
+ case "CancelTouchSequence":
+ this.cancelPending();
+ break;
+ }
+ } catch (e) {
+ Util.dumpLn("Error while handling gesture event", aEvent.type,
+ "\nPlease report error at:", e);
+ Cu.reportError(e);
+ }
+ },
+
+ /*
+ * Event handlers
+ */
+
+ cancelPending: function cancelPending() {
+ if (AnimatedZoom.isZooming())
+ AnimatedZoom.finish();
+ },
+
+ _onSwipe: function _onSwipe(aEvent) {
+ switch (aEvent.direction) {
+ case Ci.nsIDOMSimpleGestureEvent.DIRECTION_LEFT:
+ return this._tryCommand("cmd_forward");
+ case Ci.nsIDOMSimpleGestureEvent.DIRECTION_RIGHT:
+ return this._tryCommand("cmd_back");
+ }
+ return false;
+ },
+
+ _tryCommand: function _tryCommand(aId) {
+ if (document.getElementById(aId).getAttribute("disabled") == "true")
+ return false;
+ CommandUpdater.doCommand(aId);
+ return true;
+ },
+
+ _pinchStart: function _pinchStart(aEvent) {
+ if (AnimatedZoom.isZooming())
+ return;
+ // Cancel other touch sequence events, and be courteous by allowing them
+ // to say no.
+ let event = document.createEvent("Events");
+ event.initEvent("CancelTouchSequence", true, true);
+ let success = aEvent.target.dispatchEvent(event);
+
+ if (!success || !Browser.selectedTab.allowZoom)
+ return;
+
+ AnimatedZoom.start();
+ this._pinchDelta = 0;
+
+ //this._ignoreNextUpdate = true; // first update gives useless, huge delta
+
+ // cache gesture limit values
+ this._maxGrowth = Services.prefs.getIntPref("browser.ui.pinch.maxGrowth");
+ this._maxShrink = Services.prefs.getIntPref("browser.ui.pinch.maxShrink");
+ this._scalingFactor = Services.prefs.getIntPref("browser.ui.pinch.scalingFactor");
+
+ // Adjust the client coordinates to be relative to the browser element's top left corner.
+ this._browserBCR = getBrowser().getBoundingClientRect();
+ this._pinchStartX = aEvent.clientX - this._browserBCR.left;
+ this._pinchStartY = aEvent.clientY - this._browserBCR.top;
+ },
+
+ _pinchUpdate: function _pinchUpdate(aEvent) {
+ if (!AnimatedZoom.isZooming() || !aEvent.delta)
+ return;
+
+ let delta = 0;
+ let browser = AnimatedZoom.browser;
+ let oldScale = browser.scale;
+ let bcr = this._browserBCR;
+
+ // Accumulate pinch delta. Small changes are just jitter.
+ this._pinchDelta += aEvent.delta;
+ if (Math.abs(this._pinchDelta) >= oldScale) {
+ delta = this._pinchDelta;
+ this._pinchDelta = 0;
+ }
+
+ // decrease the pinchDelta min/max values to limit zooming out/in speed
+ delta = Util.clamp(delta, -this._maxShrink, this._maxGrowth);
+
+ let newScale = Browser.selectedTab.clampZoomLevel(oldScale * (1 + delta / this._scalingFactor));
+ let startScale = AnimatedZoom.startScale;
+ let scaleRatio = startScale / newScale;
+ let cX = aEvent.clientX - bcr.left;
+ let cY = aEvent.clientY - bcr.top;
+
+ // Calculate the new zoom rect.
+ let rect = AnimatedZoom.zoomFrom.clone();
+ rect.translate(this._pinchStartX - cX + (1-scaleRatio) * cX * rect.width / bcr.width,
+ this._pinchStartY - cY + (1-scaleRatio) * cY * rect.height / bcr.height);
+
+ rect.width *= scaleRatio;
+ rect.height *= scaleRatio;
+
+ this.translateInside(rect, new Rect(0, 0, browser.contentDocumentWidth * startScale,
+ browser.contentDocumentHeight * startScale));
+
+ // redraw zoom canvas according to new zoom rect
+ AnimatedZoom.updateTo(rect);
+ },
+
+ _pinchEnd: function _pinchEnd(aEvent) {
+ if (AnimatedZoom.isZooming())
+ AnimatedZoom.finish();
+ },
+
+ /**
+ * Ensure r0 is inside r1, if possible. Preserves w, h.
+ * Same as Rect.prototype.translateInside except it aligns the top left
+ * instead of the bottom right if r0 is bigger than r1.
+ */
+ translateInside: function translateInside(r0, r1) {
+ let offsetX = (r0.left < r1.left ? r1.left - r0.left :
+ (r0.right > r1.right ? Math.max(r1.left - r0.left, r1.right - r0.right) : 0));
+ let offsetY = (r0.top < r1.top ? r1.top - r0.top :
+ (r0.bottom > r1.bottom ? Math.max(r1.top - r0.top, r1.bottom - r0.bottom) : 0));
+ return r0.translate(offsetX, offsetY);
+ }
+};
+
+/**
+ * Helper to track when the user is using a precise pointing device (pen/mouse)
+ * versus an imprecise one (touch).
+ */
+var InputSourceHelper = {
+ _isPrecise: false,
+ _treatMouseAsTouch: false,
+
+ get isPrecise() {
+ return this._isPrecise;
+ },
+
+ get treatMouseAsTouch() {
+ return this._treatMouseAsTouch;
+ },
+
+ set treatMouseAsTouch(aVal) {
+ this._treatMouseAsTouch = aVal;
+ },
+
+ init: function ish_init() {
+ // debug feature, make all input imprecise
+ try {
+ this.treatMouseAsTouch = Services.prefs.getBoolPref(kDebugMouseInputPref);
+ } catch (e) {}
+ if (!this.treatMouseAsTouch) {
+ window.addEventListener("mousemove", this, true);
+ window.addEventListener("mousedown", this, true);
+ }
+ },
+
+ handleEvent: function ish_handleEvent(aEvent) {
+ switch (aEvent.mozInputSource) {
+ case Ci.nsIDOMMouseEvent.MOZ_SOURCE_MOUSE:
+ case Ci.nsIDOMMouseEvent.MOZ_SOURCE_PEN:
+ case Ci.nsIDOMMouseEvent.MOZ_SOURCE_ERASER:
+ case Ci.nsIDOMMouseEvent.MOZ_SOURCE_CURSOR:
+ if (!this._isPrecise && !this.treatMouseAsTouch) {
+ this._isPrecise = true;
+ this._fire("MozPrecisePointer");
+ }
+ break;
+
+ case Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH:
+ if (this._isPrecise) {
+ this._isPrecise = false;
+ this._fire("MozImprecisePointer");
+ }
+ break;
+ }
+ },
+
+ fireUpdate: function fireUpdate() {
+ if (this.treatMouseAsTouch) {
+ this._fire("MozImprecisePointer");
+ } else {
+ if (this._isPrecise) {
+ this._fire("MozPrecisePointer");
+ } else {
+ this._fire("MozImprecisePointer");
+ }
+ }
+ },
+
+ _fire: function (name) {
+ let event = document.createEvent("Events");
+ event.initEvent(name, true, true);
+ window.dispatchEvent(event);
+ }
+};
diff --git a/browser/metro/base/content/jsshell/shell.html b/browser/metro/base/content/jsshell/shell.html
new file mode 100644
index 000000000000..b9c0654583a8
--- /dev/null
+++ b/browser/metro/base/content/jsshell/shell.html
@@ -0,0 +1,770 @@
+
+
+
+
+
+JavaScript Shell 1.4
+
+
+
+
+
+
+
+
+
+
+
+JavaScript Shell 1.4
Features: autocompletion of property names with Tab, multiline input with Shift+Enter, input history with (Ctrl+) Up/Down,
Math,
help
+
+
+
+
+
+
diff --git a/browser/metro/base/content/jsshell/shell.xul b/browser/metro/base/content/jsshell/shell.xul
new file mode 100644
index 000000000000..5bb0cd2108b2
--- /dev/null
+++ b/browser/metro/base/content/jsshell/shell.xul
@@ -0,0 +1,13 @@
+
+
+
+
+
+
diff --git a/browser/metro/base/content/pages/aboutCertError.xhtml b/browser/metro/base/content/pages/aboutCertError.xhtml
new file mode 100644
index 000000000000..9e0ab5291fd4
--- /dev/null
+++ b/browser/metro/base/content/pages/aboutCertError.xhtml
@@ -0,0 +1,241 @@
+
+
+
+ %htmlDTD;
+
+ %globalDTD;
+
+ %certerrorDTD;
+]>
+
+
+
+
+ &certerror.pagetitle;
+
+
+
+
+
+
+
+
+
+
+
+
+
&certerror.longpagetitle;
+
+
+
+
+
+
+
+
+
&certerror.introPara1;
+
+
+
+
&certerror.whatShouldIDo.heading;
+
+
&certerror.whatShouldIDo.content;
+
+
+
+
+
+
+
&certerror.technical.heading;
+
+
+
+
+
&certerror.expert.heading;
+
+
&certerror.expert.content;
+
&certerror.expert.contentPara2;
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/metro/base/content/pages/aboutCrash.xhtml b/browser/metro/base/content/pages/aboutCrash.xhtml
new file mode 100644
index 000000000000..24a538da36d1
--- /dev/null
+++ b/browser/metro/base/content/pages/aboutCrash.xhtml
@@ -0,0 +1,32 @@
+
+
+ %htmlDTD;
+
+ %brandDTD;
+
+ %aboutRightsDTD;
+]>
+
+
+
+
+
+ Oh noooz! You crashed!
+
+
+
+
+
+Oh noooz! You crashed!
+
+A crash report is being submitted as you read this, we hope! Check about:crashes for crash reports.
+(I'm know I'm boring to look at, hopefully someday I'll be pretty and useful and stuff!)
+
+
+
+
+
diff --git a/browser/metro/base/content/pages/aboutRights.xhtml b/browser/metro/base/content/pages/aboutRights.xhtml
new file mode 100644
index 000000000000..47e41b209253
--- /dev/null
+++ b/browser/metro/base/content/pages/aboutRights.xhtml
@@ -0,0 +1,95 @@
+
+
+ %htmlDTD;
+
+ %brandDTD;
+
+ %aboutRightsDTD;
+]>
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+
+
+ &rights.pagetitle;
+
+
+
+
+
+&rights.intro-header;
+
+&rights.intro;
+
+
+ - &rights.intro-point1a;&rights.intro-point1b;&rights.intro-point1c;
+# Point 2 discusses Mozilla trademarks, and isn't needed when the build is unbranded.
+# Point 3 discusses privacy policy, unbranded builds get a placeholder (for the vendor to replace)
+# Point 4 discusses web service terms, unbranded builds gets a placeholder (for the vendor to replace)
+ - &rights.intro-point2-a;&rights.intro-point2-b;&rights.intro-point2-c;
+ - &rights.intro-point2.5;
+ - &rights2.intro-point3a;&rights2.intro-point3b;&rights.intro-point3c;
+ - &rights2.intro-point4a;&rights.intro-point4b;&rights.intro-point4c;
+
+
+
+
+
&rights2.webservices-header;
+
+
&rights2.webservices-a;&rights2.webservices-b;&rights2.webservices-c;
+
+
+
+
+
+
+
&rights.locationawarebrowsing-a;&rights.locationawarebrowsing-b;
+
+ - &rights.locationawarebrowsing-term1a;
&rights.locationawarebrowsing-term1b;
+ - &rights.locationawarebrowsing-term2;
+ - &rights.locationawarebrowsing-term3;
+ - &rights.locationawarebrowsing-term4;
+
+
+
+
+# Terms only apply to official builds, unbranded builds get a placeholder.
+ - &rights2.webservices-term1;
+ - &rights.webservices-term2;
+ - &rights2.webservices-term3;
+ - &rights.webservices-term4;
+ - &rights.webservices-term5;
+ - &rights.webservices-term6;
+ - &rights.webservices-term7;
+
+
+
+
+
+
+
diff --git a/browser/metro/base/content/pages/blockedSite.xhtml b/browser/metro/base/content/pages/blockedSite.xhtml
new file mode 100644
index 000000000000..c0979ef08f6a
--- /dev/null
+++ b/browser/metro/base/content/pages/blockedSite.xhtml
@@ -0,0 +1,192 @@
+
+
+
+ %htmlDTD;
+
+ %globalDTD;
+
+ %brandDTD;
+
+ %blockedSiteDTD;
+]>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
&safeb.blocked.phishingPage.title2;
+ &safeb.blocked.malwarePage.title;
+
+
+
+
+
+
+
+
+
&safeb.blocked.phishingPage.shortDesc2;
+
&safeb.blocked.malwarePage.shortDesc;
+
+
+
+
+
&safeb.blocked.phishingPage.longDesc2;
+
&safeb.blocked.malwarePage.longDesc;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/metro/base/content/pages/fullscreen-video.xhtml b/browser/metro/base/content/pages/fullscreen-video.xhtml
new file mode 100644
index 000000000000..48694b16496e
--- /dev/null
+++ b/browser/metro/base/content/pages/fullscreen-video.xhtml
@@ -0,0 +1,154 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/metro/base/content/pages/netError.xhtml b/browser/metro/base/content/pages/netError.xhtml
new file mode 100644
index 000000000000..52b974f736a1
--- /dev/null
+++ b/browser/metro/base/content/pages/netError.xhtml
@@ -0,0 +1,365 @@
+
+
+
+ %htmlDTD;
+
+ %netErrorDTD;
+
+ %globalDTD;
+]>
+
+
+
+
+
+
+ &loadError.label;
+
+
+
+
+
+
+
+
+
+
+
+
+
&generic.title;
+ &dnsNotFound.title;
+ &fileNotFound.title;
+
+ &protocolNotFound.title;
+ &connectionFailure.title;
+ &netTimeout.title;
+ &redirectLoop.title;
+ &unknownSocketType.title;
+ &netReset.title;
+ ¬Cached.title;
+ &netOffline.title;
+ &netInterrupt.title;
+ &deniedPortAccess.title;
+ &proxyResolveFailure.title;
+ &proxyConnectFailure.title;
+ &contentEncodingError.title;
+ &unsafeContentType.title;
+ &nssFailure2.title;
+ &nssBadCert.title;
+ &cspFrameAncestorBlocked.title;
+ &remoteXUL.title;
+ &corruptedContentError.title;
+
+
+
&generic.longDesc;
+
&dnsNotFound.longDesc;
+
&fileNotFound.longDesc;
+
&malformedURI.longDesc;
+
&protocolNotFound.longDesc;
+
&connectionFailure.longDesc;
+
&netTimeout.longDesc;
+
&redirectLoop.longDesc;
+
&unknownSocketType.longDesc;
+
&netReset.longDesc;
+
¬Cached.longDesc;
+
&netOffline.longDesc2;
+
&netInterrupt.longDesc;
+
&deniedPortAccess.longDesc;
+
&proxyResolveFailure.longDesc;
+
&proxyConnectFailure.longDesc;
+
&contentEncodingError.longDesc;
+
&unsafeContentType.longDesc;
+
&nssFailure2.longDesc;
+
&nssBadCert.longDesc2;
+
&cspFrameAncestorBlocked.longDesc;
+
&remoteXUL.longDesc;
+
&corruptedContentError.longDesc;
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/metro/base/content/preferences.js b/browser/metro/base/content/preferences.js
new file mode 100644
index 000000000000..b6890d194373
--- /dev/null
+++ b/browser/metro/base/content/preferences.js
@@ -0,0 +1,17 @@
+// -*- Mode: js2; tab-width: 2; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var PreferencesPanelView = {
+ init: function pv_init() {
+ // Run some setup code the first time the panel is shown.
+ Elements.prefsFlyout.addEventListener("PopupChanged", function onShow(aEvent) {
+ if (aEvent.detail && aEvent.popup === Elements.prefsFlyout) {
+ Elements.prefsFlyout.removeEventListener("PopupChanged", onShow, false);
+ MasterPasswordUI.updatePreference();
+ WeaveGlue.init();
+ }
+ }, false);
+ }
+};
diff --git a/browser/metro/base/content/prompt/CaptureDialog.xul b/browser/metro/base/content/prompt/CaptureDialog.xul
new file mode 100644
index 000000000000..210c6aef66c9
--- /dev/null
+++ b/browser/metro/base/content/prompt/CaptureDialog.xul
@@ -0,0 +1,40 @@
+
+
+
+
+
diff --git a/browser/metro/base/content/prompt/alert.xul b/browser/metro/base/content/prompt/alert.xul
new file mode 100644
index 000000000000..177615a0c3fb
--- /dev/null
+++ b/browser/metro/base/content/prompt/alert.xul
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
diff --git a/browser/metro/base/content/prompt/confirm.xul b/browser/metro/base/content/prompt/confirm.xul
new file mode 100644
index 000000000000..c43ee24e6db0
--- /dev/null
+++ b/browser/metro/base/content/prompt/confirm.xul
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
diff --git a/browser/metro/base/content/prompt/masterPassword.xul b/browser/metro/base/content/prompt/masterPassword.xul
new file mode 100644
index 000000000000..0a2a68d1ac1d
--- /dev/null
+++ b/browser/metro/base/content/prompt/masterPassword.xul
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+%dialog;
+%changempDTD;
+]>
+
+
diff --git a/browser/metro/base/content/prompt/prompt.js b/browser/metro/base/content/prompt/prompt.js
new file mode 100644
index 000000000000..eae208634220
--- /dev/null
+++ b/browser/metro/base/content/prompt/prompt.js
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const PromptHelper = {
+ closeDialog: function(confirm, id) {
+ let dialog = document.getElementById(id);
+ if (typeof confirm == "boolean" && dialog.arguments && "defaultButton" in dialog.arguments)
+ // confirmEx always returns 1 when dismissed with "escape" (bug 345067).
+ dialog.arguments.result = confirm ? dialog.arguments.defaultButton : 1;
+ else
+ dialog.arguments.result = confirm;
+ dialog.close();
+ },
+
+ // Alert dialog
+ onCloseAlert: function(dialog) {
+ if (dialog.arguments)
+ dialog.arguments.value = document.getElementById("prompt-alert-checkbox").checked;
+ },
+
+ // Confirm dialog
+ closeConfirm: function(confirm) {
+ this.closeDialog(confirm, "prompt-confirm-dialog");
+ },
+
+ onCloseConfirm: function(dialog) {
+ if (dialog.arguments && ("checkbox" in dialog.arguments))
+ dialog.arguments.checkbox.value = document.getElementById("prompt-confirm-checkbox").checked;
+ },
+
+ // Prompt dialog
+ closePrompt: function(confirm) {
+ this.closeDialog(confirm, "prompt-prompt-dialog");
+ },
+
+ onClosePrompt: function(dialog) {
+ if (dialog.arguments) {
+ dialog.arguments.checkbox.value = document.getElementById("prompt-prompt-checkbox").checked;
+ dialog.arguments.value.value = document.getElementById("prompt-prompt-textbox").value;
+ }
+ },
+
+ // User / Password dialog
+ onLoadPassword: function onLoadPassword(dialog) {
+ let user = document.getElementById('prompt-password-user');
+ if (!user.value)
+ user.focus();
+ },
+
+ closePassword: function(confirm) {
+ this.closeDialog(confirm, "prompt-password-dialog");
+ },
+
+ onClosePassword: function(dialog) {
+ if (dialog.arguments) {
+ dialog.arguments.checkbox.value = document.getElementById("prompt-password-checkbox").checked;
+ dialog.arguments.user.value = document.getElementById("prompt-password-user").value;
+ dialog.arguments.password.value = document.getElementById("prompt-password-password").value;
+ }
+ },
+
+ // Select dialog
+ closeSelect: function(confirm) {
+ this.closeDialog(confirm, "prompt-select-dialog");
+ },
+
+ onCloseSelect: function(dialog) {
+ if (dialog.arguments)
+ dialog.arguments.selection.value = document.getElementById("prompt-select-list").selectedIndex;
+ }
+};
diff --git a/browser/metro/base/content/prompt/prompt.xul b/browser/metro/base/content/prompt/prompt.xul
new file mode 100644
index 000000000000..6979fdb74975
--- /dev/null
+++ b/browser/metro/base/content/prompt/prompt.xul
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+
diff --git a/browser/metro/base/content/prompt/promptPassword.xul b/browser/metro/base/content/prompt/promptPassword.xul
new file mode 100644
index 000000000000..97c3aeb5fc80
--- /dev/null
+++ b/browser/metro/base/content/prompt/promptPassword.xul
@@ -0,0 +1,63 @@
+
+
+
+
+
+
+ %promptDTD;
+ %commonDialogDTD;
+]>
+
+
diff --git a/browser/metro/base/content/prompt/removeMasterPassword.xul b/browser/metro/base/content/prompt/removeMasterPassword.xul
new file mode 100644
index 000000000000..95e07b3ba116
--- /dev/null
+++ b/browser/metro/base/content/prompt/removeMasterPassword.xul
@@ -0,0 +1,43 @@
+
+
+
+
+
+
+%dialog;
+%removempDTD;
+]>
+
+
diff --git a/browser/metro/base/content/prompt/select.xul b/browser/metro/base/content/prompt/select.xul
new file mode 100644
index 000000000000..3dd7202cc5e6
--- /dev/null
+++ b/browser/metro/base/content/prompt/select.xul
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
diff --git a/browser/metro/base/content/prompt/share.xul b/browser/metro/base/content/prompt/share.xul
new file mode 100644
index 000000000000..8e5359d64cc4
--- /dev/null
+++ b/browser/metro/base/content/prompt/share.xul
@@ -0,0 +1,27 @@
+
+
+
+
+
+
+
diff --git a/browser/metro/base/content/sanitize.js b/browser/metro/base/content/sanitize.js
new file mode 100644
index 000000000000..56e715977ac5
--- /dev/null
+++ b/browser/metro/base/content/sanitize.js
@@ -0,0 +1,315 @@
+// -*- Mode: Java; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*-
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function Sanitizer() {}
+Sanitizer.prototype = {
+ // warning to the caller: this one may raise an exception (e.g. bug #265028)
+ clearItem: function (aItemName)
+ {
+ if (this.items[aItemName].canClear)
+ this.items[aItemName].clear();
+ },
+
+ canClearItem: function (aItemName)
+ {
+ return this.items[aItemName].canClear;
+ },
+
+ _prefDomain: "privacy.item.",
+ getNameFromPreference: function (aPreferenceName)
+ {
+ return aPreferenceName.substr(this._prefDomain.length);
+ },
+
+ /**
+ * Deletes privacy sensitive data in a batch, according to user preferences
+ *
+ * @returns null if everything's fine; an object in the form
+ * { itemName: error, ... } on (partial) failure
+ */
+ sanitize: function ()
+ {
+ var branch = Services.prefs.getBranch(this._prefDomain);
+ var errors = null;
+ for (var itemName in this.items) {
+ var item = this.items[itemName];
+ if ("clear" in item && item.canClear && branch.getBoolPref(itemName)) {
+ // Some of these clear() may raise exceptions (see bug #265028)
+ // to sanitize as much as possible, we catch and store them,
+ // rather than fail fast.
+ // Callers should check returned errors and give user feedback
+ // about items that could not be sanitized
+ try {
+ item.clear();
+ } catch(er) {
+ if (!errors)
+ errors = {};
+ errors[itemName] = er;
+ dump("Error sanitizing " + itemName + ": " + er + "\n");
+ }
+ }
+ }
+ return errors;
+ },
+
+ items: {
+ // Clear Sync account before passwords so that Sync still has access to the
+ // credentials to clean up device-specific records on the server. Also
+ // disable it before wiping history so we don't accidentally sync that.
+ syncAccount: {
+ clear: function ()
+ {
+ WeaveGlue.disconnect();
+ },
+
+ get canClear()
+ {
+ return (Weave.Status.checkSetup() != Weave.CLIENT_NOT_CONFIGURED);
+ }
+ },
+
+ cache: {
+ clear: function ()
+ {
+ var cacheService = Cc["@mozilla.org/network/cache-service;1"].getService(Ci.nsICacheService);
+ try {
+ cacheService.evictEntries(Ci.nsICache.STORE_ANYWHERE);
+ } catch(er) {}
+
+ let imageCache = Cc["@mozilla.org/image/cache;1"].getService(Ci.imgICache);
+ try {
+ imageCache.clearCache(false); // true=chrome, false=content
+ } catch(er) {}
+ },
+
+ get canClear()
+ {
+ return true;
+ }
+ },
+
+ cookies: {
+ clear: function ()
+ {
+ var cookieMgr = Cc["@mozilla.org/cookiemanager;1"].getService(Ci.nsICookieManager);
+ cookieMgr.removeAll();
+ },
+
+ get canClear()
+ {
+ return true;
+ }
+ },
+
+ geolocation: {
+ clear: function ()
+ {
+ // clear any network geolocation provider sessions
+ try {
+ var branch = Services.prefs.getBranch("geo.wifi.access_token.");
+ branch.deleteBranch("");
+
+ branch = Services.prefs.getBranch("geo.request.remember.");
+ branch.deleteBranch("");
+ } catch (e) {dump(e);}
+ },
+
+ get canClear()
+ {
+ return true;
+ }
+ },
+
+ siteSettings: {
+ clear: function ()
+ {
+ // Clear site-specific permissions like "Allow this site to open popups"
+ Services.perms.removeAll();
+
+ // Clear site-specific settings like page-zoom level
+ var cps = Cc["@mozilla.org/content-pref/service;1"].getService(Ci.nsIContentPrefService);
+ cps.removeGroupedPrefs();
+
+ // Clear "Never remember passwords for this site", which is not handled by
+ // the permission manager
+ var pwmgr = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
+ var hosts = pwmgr.getAllDisabledHosts({})
+ for each (var host in hosts) {
+ pwmgr.setLoginSavingEnabled(host, true);
+ }
+ },
+
+ get canClear()
+ {
+ return true;
+ }
+ },
+
+ offlineApps: {
+ clear: function ()
+ {
+ var cacheService = Cc["@mozilla.org/network/cache-service;1"].getService(Ci.nsICacheService);
+ try {
+ cacheService.evictEntries(Ci.nsICache.STORE_OFFLINE);
+ } catch(er) {}
+ },
+
+ get canClear()
+ {
+ return true;
+ }
+ },
+
+ history: {
+ clear: function ()
+ {
+ var globalHistory = Cc["@mozilla.org/browser/global-history;2"].getService(Ci.nsIBrowserHistory);
+ globalHistory.removeAllPages();
+
+ try {
+ Services.obs.notifyObservers(null, "browser:purge-session-history", "");
+ }
+ catch (e) { }
+
+ // Clear last URL of the Open Web Location dialog
+ try {
+ Services.prefs.clearUserPref("general.open_location.last_url");
+ }
+ catch (e) { }
+ },
+
+ get canClear()
+ {
+ // bug 347231: Always allow clearing history due to dependencies on
+ // the browser:purge-session-history notification. (like error console)
+ return true;
+ }
+ },
+
+ formdata: {
+ clear: function ()
+ {
+ //Clear undo history of all searchBars
+ var windows = Services.wm.getEnumerator("navigator:browser");
+ while (windows.hasMoreElements()) {
+ var searchBar = windows.getNext().document.getElementById("searchbar");
+ if (searchBar) {
+ searchBar.value = "";
+ searchBar.textbox.editor.transactionManager.clear();
+ }
+ }
+
+ var formHistory = Cc["@mozilla.org/satchel/form-history;1"].getService(Ci.nsIFormHistory2);
+ formHistory.removeAllEntries();
+ },
+
+ get canClear()
+ {
+ var formHistory = Cc["@mozilla.org/satchel/form-history;1"].getService(Ci.nsIFormHistory2);
+ return formHistory.hasEntries;
+ }
+ },
+
+ downloads: {
+ clear: function ()
+ {
+ var dlMgr = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
+ dlMgr.cleanUp();
+ },
+
+ get canClear()
+ {
+ var dlMgr = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
+ return dlMgr.canCleanUp;
+ }
+ },
+
+ passwords: {
+ clear: function ()
+ {
+ var pwmgr = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
+ pwmgr.removeAllLogins();
+ },
+
+ get canClear()
+ {
+ var pwmgr = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
+ var count = pwmgr.countLogins("", "", ""); // count all logins
+ return (count > 0);
+ }
+ },
+
+ sessions: {
+ clear: function ()
+ {
+ // clear all auth tokens
+ var sdr = Cc["@mozilla.org/security/sdr;1"].getService(Ci.nsISecretDecoderRing);
+ sdr.logoutAndTeardown();
+
+ // clear plain HTTP auth sessions
+ var authMgr = Cc['@mozilla.org/network/http-auth-manager;1'].getService(Ci.nsIHttpAuthManager);
+ authMgr.clearAll();
+ },
+
+ get canClear()
+ {
+ return true;
+ }
+ }
+ }
+};
+
+
+// "Static" members
+Sanitizer.prefDomain = "privacy.sanitize.";
+Sanitizer.prefShutdown = "sanitizeOnShutdown";
+Sanitizer.prefDidShutdown = "didShutdownSanitize";
+
+Sanitizer._prefs = null;
+Sanitizer.__defineGetter__("prefs", function()
+{
+ return Sanitizer._prefs ? Sanitizer._prefs
+ : Sanitizer._prefs = Cc["@mozilla.org/preferences-service;1"]
+ .getService(Ci.nsIPrefService)
+ .getBranch(Sanitizer.prefDomain);
+});
+
+/**
+ * Deletes privacy sensitive data in a batch, optionally showing the
+ * sanitize UI, according to user preferences
+ *
+ * @returns null if everything's fine
+ * an object in the form { itemName: error, ... } on (partial) failure
+ */
+Sanitizer.sanitize = function()
+{
+ return new Sanitizer().sanitize();
+};
+
+Sanitizer.onStartup = function()
+{
+ // we check for unclean exit with pending sanitization
+ Sanitizer._checkAndSanitize();
+};
+
+Sanitizer.onShutdown = function()
+{
+ // we check if sanitization is needed and perform it
+ Sanitizer._checkAndSanitize();
+};
+
+// this is called on startup and shutdown, to perform pending sanitizations
+Sanitizer._checkAndSanitize = function()
+{
+ const prefs = Sanitizer.prefs;
+ if (prefs.getBoolPref(Sanitizer.prefShutdown) &&
+ !prefs.prefHasUserValue(Sanitizer.prefDidShutdown)) {
+ // this is a shutdown or a startup after an unclean exit
+ Sanitizer.sanitize() || // sanitize() returns null on full success
+ prefs.setBoolPref(Sanitizer.prefDidShutdown, true);
+ }
+};
+
+
diff --git a/browser/metro/base/content/sync.js b/browser/metro/base/content/sync.js
new file mode 100644
index 000000000000..c19178f7d79b
--- /dev/null
+++ b/browser/metro/base/content/sync.js
@@ -0,0 +1,678 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+let WeaveGlue = {
+ setupData: null,
+ _boundOnEngineSync: null, // Needed to unhook the observers in close().
+ _boundOnServiceSync: null,
+ jpake: null,
+ _bundle: null,
+ _loginError: false,
+ _progressBar: null,
+ _progressValue: 0,
+ _progressMax: null,
+
+ init: function init() {
+ if (this._bundle) {
+ return;
+ }
+
+ let service = Components.classes["@mozilla.org/weave/service;1"]
+ .getService(Components.interfaces.nsISupports)
+ .wrappedJSObject;
+
+ if (service.ready) {
+ this._init();
+ return;
+ }
+
+ Services.obs.addObserver(function onReady() {
+ Services.obs.removeObserver(onReady, "weave:service:ready");
+ this._init();
+ }.bind(this), "weave:service:ready", false);
+
+ service.ensureLoaded();
+ },
+
+ _init: function () {
+ this._bundle = Services.strings.createBundle("chrome://browser/locale/sync.properties");
+ this._msg = document.getElementById("prefs-messages");
+
+ this._addListeners();
+
+ this.setupData = { account: "", password: "" , synckey: "", serverURL: "" };
+
+ if (Weave.Status.checkSetup() != Weave.CLIENT_NOT_CONFIGURED) {
+ // Put the settings UI into a state of "connecting..." if we are going to auto-connect
+ this._elements.connect.firstChild.disabled = true;
+ this._elements.connect.setAttribute("title", this._bundle.GetStringFromName("connecting.label"));
+
+ try {
+ this._elements.device.value = Services.prefs.getCharPref("services.sync.client.name");
+ } catch(e) {}
+ } else if (Weave.Status.login != Weave.LOGIN_FAILED_NO_USERNAME) {
+ this.loadSetupData();
+ }
+ this._boundOnEngineSync = this.onEngineSync.bind(this);
+ this._boundOnServiceSync = this.onServiceSync.bind(this);
+ this._progressBar = document.getElementById("syncsetup-progressbar");
+ },
+
+ abortEasySetup: function abortEasySetup() {
+ document.getElementById("syncsetup-code1").value = "....";
+ document.getElementById("syncsetup-code2").value = "....";
+ document.getElementById("syncsetup-code3").value = "....";
+ if (!this.jpake)
+ return;
+
+ this.jpake.abort();
+ this.jpake = null;
+ },
+
+ _resetScrollPosition: function _resetScrollPosition() {
+ let scrollboxes = document.getElementsByClassName("syncsetup-scrollbox");
+ for (let i = 0; i < scrollboxes.length; i++) {
+ let sbo = scrollboxes[i].boxObject.QueryInterface(Ci.nsIScrollBoxObject);
+ try {
+ sbo.scrollTo(0, 0);
+ } catch(e) {}
+ }
+ },
+
+ open: function open() {
+ let container = document.getElementById("syncsetup-container");
+ if (!container.hidden)
+ return;
+
+ // Services.io.offline is lying to us, so we use the NetworkLinkService instead
+ let nls = Cc["@mozilla.org/network/network-link-service;1"].getService(Ci.nsINetworkLinkService);
+ if (!nls.isLinkUp) {
+ Services.obs.notifyObservers(null, "browser:sync:setup:networkerror", "");
+ Services.prompt.alert(window,
+ this._bundle.GetStringFromName("sync.setup.error.title"),
+ this._bundle.GetStringFromName("sync.setup.error.network"));
+ return;
+ }
+
+ // Clear up any previous JPAKE codes
+ this.abortEasySetup();
+
+ // Show the connect UI
+ container.hidden = false;
+ document.getElementById("syncsetup-simple").hidden = false;
+ document.getElementById("syncsetup-waiting").hidden = true;
+ document.getElementById("syncsetup-fallback").hidden = true;
+
+ DialogUI.pushDialog(this);
+
+ let self = this;
+ this.jpake = new Weave.JPAKEClient({
+ displayPIN: function displayPIN(aPin) {
+ document.getElementById("syncsetup-code1").value = aPin.slice(0, 4);
+ document.getElementById("syncsetup-code2").value = aPin.slice(4, 8);
+ document.getElementById("syncsetup-code3").value = aPin.slice(8);
+ },
+
+ onPairingStart: function onPairingStart() {
+ document.getElementById("syncsetup-simple").hidden = true;
+ document.getElementById("syncsetup-waiting").hidden = false;
+ },
+
+ onComplete: function onComplete(aCredentials) {
+ self.jpake = null;
+
+ self._progressBar.mode = "determined";
+ document.getElementById("syncsetup-waiting-desc").hidden = true;
+ document.getElementById("syncsetup-waiting-cancel").hidden = true;
+ document.getElementById("syncsetup-waitingdownload-desc").hidden = false;
+ document.getElementById("syncsetup-waiting-close").hidden = false;
+ Services.obs.addObserver(self._boundOnEngineSync, "weave:engine:sync:finish", false);
+ Services.obs.addObserver(self._boundOnEngineSync, "weave:engine:sync:error", false);
+ Services.obs.addObserver(self._boundOnServiceSync, "weave:service:sync:finish", false);
+ Services.obs.addObserver(self._boundOnServiceSync, "weave:service:sync:error", false);
+ self.setupData = aCredentials;
+ self.connect();
+ },
+
+ onAbort: function onAbort(aError) {
+ self.jpake = null;
+
+ if (aError == "jpake.error.userabort" || container.hidden) {
+ Services.obs.notifyObservers(null, "browser:sync:setup:userabort", "");
+ return;
+ }
+
+ // Automatically go to manual setup if we couldn't acquire a channel.
+ let brandShortName = Strings.brand.GetStringFromName("brandShortName");
+ let tryAgain = self._bundle.GetStringFromName("sync.setup.tryagain");
+ let manualSetup = self._bundle.GetStringFromName("sync.setup.manual");
+ let buttonFlags = Ci.nsIPrompt.BUTTON_POS_1_DEFAULT +
+ (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) +
+ (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1) +
+ (Ci.nsIPrompt.BUTTON_TITLE_CANCEL * Ci.nsIPrompt.BUTTON_POS_2);
+
+ let button = Services.prompt.confirmEx(window,
+ self._bundle.GetStringFromName("sync.setup.error.title"),
+ self._bundle.formatStringFromName("sync.setup.error.nodata", [brandShortName], 1),
+ buttonFlags, tryAgain, manualSetup, null, "", {});
+ switch (button) {
+ case 0:
+ // we have to build a new JPAKEClient here rather than reuse the old one
+ container.hidden = true;
+ self.open();
+ break;
+ case 1:
+ self.openManual();
+ break;
+ case 2:
+ default:
+ self.close();
+ break;
+ }
+ }
+ });
+ this.jpake.receiveNoPIN();
+ },
+
+ openManual: function openManual() {
+ this.abortEasySetup();
+
+ // Reset the scroll since the previous page might have been scrolled
+ this._resetScrollPosition();
+
+ document.getElementById("syncsetup-simple").hidden = true;
+ document.getElementById("syncsetup-waiting").hidden = true;
+ document.getElementById("syncsetup-fallback").hidden = false;
+
+ // Push the current setup data into the UI
+ if (this.setupData && "account" in this.setupData) {
+ this._elements.account.value = this.setupData.account;
+ this._elements.password.value = this.setupData.password;
+ let pp = this.setupData.synckey;
+ if (Weave.Utils.isPassphrase(pp))
+ pp = Weave.Utils.hyphenatePassphrase(pp);
+ this._elements.synckey.value = pp;
+ if (this.setupData.serverURL && this.setupData.serverURL.length) {
+ this._elements.usecustomserver.checked = true;
+ this._elements.customserver.disabled = false;
+ this._elements.customserver.value = this.setupData.serverURL;
+ } else {
+ this._elements.usecustomserver.checked = false;
+ this._elements.customserver.disabled = true;
+ this._elements.customserver.value = "";
+ }
+ }
+
+ this.canConnect();
+ },
+
+ onEngineSync: function onEngineSync(subject, topic, data) {
+ // The Clients engine syncs first. At this point we don't necessarily know
+ // yet how many engines will be enabled, so we'll ignore the Clients engine
+ // and evaluate how many engines are enabled when the first "real" engine
+ // syncs.
+ if (data == 'clients') {
+ return;
+ }
+ if (this._progressMax == null) {
+ this._progressMax = Weave.Service.engineManager.getEnabled().length;
+ this._progressBar.max = this._progressMax;
+ }
+ this._progressValue += 1;
+ this._progressBar.setAttribute("value", this._progressValue);
+ },
+
+ onServiceSync: function onServiceSync() {
+ this.close();
+ },
+
+ close: function close() {
+ try {
+ Services.obs.removeObserver(this._boundOnEngineSync, "weave:engine:sync:finish");
+ Services.obs.removeObserver(this._boundOnEngineSync, "weave:engine:sync:error");
+ Services.obs.removeObserver(this._boundOnServiceSync, "weave:service:sync:finish");
+ Services.obs.removeObserver(this._boundOnServiceSync, "weave:service:sync:error");
+ }
+ catch(e) {
+ // Observers weren't registered because we never got as far as onComplete.
+ }
+
+ if (this.jpake)
+ this.abortEasySetup();
+
+ // Reset the scroll since the previous page might have been scrolled
+ this._resetScrollPosition();
+
+ // Save current setup data
+ this.setupData = {
+ account: this._elements.account.value.trim(),
+ password: this._elements.password.value.trim(),
+ synckey: Weave.Utils.normalizePassphrase(this._elements.synckey.value.trim()),
+ serverURL: this._validateServer(this._elements.customserver.value.trim())
+ };
+
+ // Clear the UI so it's ready for next time
+ this._elements.account.value = "";
+ this._elements.password.value = "";
+ this._elements.synckey.value = "";
+ this._elements.usecustomserver.checked = false;
+ this._elements.customserver.disabled = true;
+ this._elements.customserver.value = "";
+ document.getElementById("syncsetup-waiting-desc").hidden = false;
+ document.getElementById("syncsetup-waiting-cancel").hidden = false;
+ document.getElementById("syncsetup-waitingdownload-desc").hidden = true;
+ document.getElementById("syncsetup-waiting-close").hidden = true;
+ this._progressMax = null;
+ this._progressValue = 0;
+ this._progressBar.max = 0;
+ this._progressBar.value = 0;
+ this._progressBar.mode = "undetermined";
+
+ // Close the connect UI
+ document.getElementById("syncsetup-container").hidden = true;
+ DialogUI.popDialog();
+ },
+
+ toggleCustomServer: function toggleCustomServer() {
+ let useCustomServer = this._elements.usecustomserver.checked;
+ this._elements.customserver.disabled = !useCustomServer;
+ if (!useCustomServer)
+ this._elements.customserver.value = "";
+ },
+
+ canConnect: function canConnect() {
+ let account = this._elements.account.value;
+ let password = this._elements.password.value;
+ let synckey = this._elements.synckey.value;
+
+ let disabled = !(account && password && synckey);
+ document.getElementById("syncsetup-button-connect").disabled = disabled;
+ },
+
+ showDetails: function showDetails() {
+ // Show the connect UI detail settings
+ let show = this._elements.sync.collapsed;
+ this._elements.details.checked = show;
+ this._elements.sync.collapsed = !show;
+ this._elements.device.collapsed = !show;
+ this._elements.disconnect.collapsed = !show;
+ },
+
+ tryConnect: function login() {
+ // If Sync is not configured, simply show the setup dialog
+ if (this._loginError || Weave.Status.checkSetup() == Weave.CLIENT_NOT_CONFIGURED) {
+ this.open();
+ return;
+ }
+
+ // No setup data, do nothing
+ if (!this.setupData)
+ return;
+
+ if (this.setupData.serverURL && this.setupData.serverURL.length)
+ Weave.Service.serverURL = this.setupData.serverURL;
+
+ // We might still be in the middle of a sync from before Sync was disabled, so
+ // let's force the UI into a state that the Sync code feels comfortable
+ this.observe(null, "", "");
+
+ // Now try to re-connect. If successful, this will reset the UI into the
+ // correct state automatically.
+ Weave.Service.login(Weave.Service.identity.username, this.setupData.password, this.setupData.synckey);
+ },
+
+ connect: function connect(aSetupData) {
+ // Use setup data to pre-configure manual fields
+ if (aSetupData)
+ this.setupData = aSetupData;
+
+ // Cause the Sync system to reset internals if we seem to be switching accounts
+ if (this.setupData.account != Weave.Service.identity.account)
+ Weave.Service.startOver();
+
+ // Remove any leftover connection error string
+ this._elements.connect.removeAttribute("desc");
+
+ // Reset the custom server URL, if we have one
+ if (this.setupData.serverURL && this.setupData.serverURL.length)
+ Weave.Service.serverURL = this.setupData.serverURL;
+
+ // Sync will use the account value and munge it into a username, as needed
+ Weave.Service.identity.account = this.setupData.account;
+ Weave.Service.identity.basicPassword = this.setupData.password;
+ Weave.Service.identity.syncKey = this.setupData.synckey;
+ Weave.Service.persistLogin();
+ Weave.Svc.Obs.notify("weave:service:setup-complete");
+ setTimeout(function () { Weave.Service.sync(); }, 0);
+ },
+
+ disconnect: function disconnect() {
+ // Save credentials for undo
+ let undoData = this.setupData;
+
+ // Remove all credentials
+ this.setupData = null;
+ Weave.Service.startOver();
+
+ let message = this._bundle.GetStringFromName("notificationDisconnect.label");
+ let button = this._bundle.GetStringFromName("notificationDisconnect.button");
+ let buttons = [ {
+ label: button,
+ accessKey: "",
+ callback: function() { WeaveGlue.connect(undoData); }
+ } ];
+ this.showMessage(message, "undo-disconnect", buttons);
+
+ // Hide the notification when the panel is changed or closed.
+ let panel = document.getElementById("prefs-container");
+ panel.addEventListener("ToolPanelHidden", function onHide(aEvent) {
+ panel.removeEventListener(aEvent.type, onHide, false);
+ let notification = WeaveGlue._msg.getNotificationWithValue("undo-disconnect");
+ if (notification)
+ notification.close();
+ }, false);
+
+ Weave.Service.logout();
+ },
+
+ sync: function sync() {
+ Weave.Service.sync();
+ },
+
+ _addListeners: function _addListeners() {
+ let topics = ["weave:service:setup-complete",
+ "weave:service:sync:start", "weave:service:sync:finish",
+ "weave:service:sync:error", "weave:service:login:start",
+ "weave:service:login:finish", "weave:service:login:error",
+ "weave:ui:login:error",
+ "weave:service:logout:finish"];
+
+ // For each topic, add WeaveGlue the observer
+ topics.forEach(function(topic) {
+ Services.obs.addObserver(WeaveGlue, topic, false);
+ });
+
+ // Remove them on unload
+ addEventListener("unload", function() {
+ topics.forEach(function(topic) {
+ Services.obs.removeObserver(WeaveGlue, topic, false);
+ });
+ }, false);
+ },
+
+ get _elements() {
+ // Do a quick test to see if the options exist yet
+ let syncButton = document.getElementById("sync-syncButton");
+ if (syncButton == null)
+ return null;
+
+ // Get all the setting nodes from the add-ons display
+ let elements = {};
+ let setupids = ["account", "password", "synckey", "usecustomserver", "customserver"];
+ setupids.forEach(function(id) {
+ elements[id] = document.getElementById("syncsetup-" + id);
+ });
+
+ let settingids = ["device", "connect", "connected", "disconnect", "sync", "details", "pairdevice"];
+ settingids.forEach(function(id) {
+ elements[id] = document.getElementById("sync-" + id);
+ });
+
+ // Replace the getter with the collection of settings
+ delete this._elements;
+ return this._elements = elements;
+ },
+
+ observe: function observe(aSubject, aTopic, aData) {
+ // Make sure we're online when connecting/syncing
+ Util.forceOnline();
+
+ // Can't do anything before settings are loaded
+ if (this._elements == null)
+ return;
+
+ // Make some aliases
+ let connect = this._elements.connect;
+ let connected = this._elements.connected;
+ let details = this._elements.details;
+ let device = this._elements.device;
+ let disconnect = this._elements.disconnect;
+ let sync = this._elements.sync;
+ let pairdevice = this._elements.pairdevice;
+
+ // Show what went wrong with login if necessary
+ if (aTopic == "weave:ui:login:error") {
+ this._loginError = true;
+ connect.setAttribute("desc", Weave.Utils.getErrorString(Weave.Status.login));
+ } else {
+ connect.removeAttribute("desc");
+ }
+
+ if (aTopic == "weave:service:login:finish") {
+ this._loginError = false;
+ // Init the setup data if we just logged in
+ if (!this.setupData)
+ this.loadSetupData();
+ }
+
+ let isConfigured = (!this._loginError && Weave.Status.checkSetup() != Weave.CLIENT_NOT_CONFIGURED);
+
+ connect.collapsed = isConfigured;
+ connected.collapsed = !isConfigured;
+
+ if (!isConfigured) {
+ connect.setAttribute("title", this._bundle.GetStringFromName("notconnected.label"));
+ connect.firstChild.disabled = false;
+ details.checked = false;
+ sync.collapsed = true;
+ device.collapsed = true;
+ disconnect.collapsed = true;
+ }
+
+ // Check the lock on a timeout because it's set just after notifying
+ setTimeout(function(self) {
+ // Prevent certain actions when the service is locked
+ if (Weave.Service.locked) {
+ connect.firstChild.disabled = true;
+ sync.firstChild.disabled = true;
+
+ if (aTopic == "weave:service:login:start")
+ connect.setAttribute("title", self._bundle.GetStringFromName("connecting.label"));
+
+ if (aTopic == "weave:service:sync:start")
+ sync.setAttribute("title", self._bundle.GetStringFromName("lastSyncInProgress2.label"));
+ } else {
+ connect.firstChild.disabled = false;
+ sync.firstChild.disabled = false;
+ }
+ }, 0, this);
+
+ // Dynamically generate some strings
+ let accountStr = this._bundle.formatStringFromName("account.label", [Weave.Service.identity.account], 1);
+ disconnect.setAttribute("title", accountStr);
+
+ // Show the day-of-week and time (HH:MM) of last sync
+ let lastSync = Weave.Svc.Prefs.get("lastSync");
+ if (lastSync != null) {
+ let syncDate = new Date(lastSync).toLocaleFormat("%a %H:%M");
+ let dateStr = this._bundle.formatStringFromName("lastSync2.label", [syncDate], 1);
+ sync.setAttribute("title", dateStr);
+ }
+
+ // Check for a storage format update, update the user and load the Sync update page
+ if (aTopic =="weave:service:sync:error") {
+ let clientOutdated = false, remoteOutdated = false;
+ if (Weave.Status.sync == Weave.VERSION_OUT_OF_DATE) {
+ clientOutdated = true;
+ } else if (Weave.Status.sync == Weave.DESKTOP_VERSION_OUT_OF_DATE) {
+ remoteOutdated = true;
+ } else if (Weave.Status.service == Weave.SYNC_FAILED_PARTIAL) {
+ // Some engines failed, check for per-engine compat
+ for (let [engine, reason] in Iterator(Weave.Status.engines)) {
+ clientOutdated = clientOutdated || reason == Weave.VERSION_OUT_OF_DATE;
+ remoteOutdated = remoteOutdated || reason == Weave.DESKTOP_VERSION_OUT_OF_DATE;
+ }
+ }
+
+ if (clientOutdated || remoteOutdated) {
+ let brand = Services.strings.createBundle("chrome://branding/locale/brand.properties");
+ let brandName = brand.GetStringFromName("brandShortName");
+
+ let type = clientOutdated ? "client" : "remote";
+ let message = this._bundle.GetStringFromName("sync.update." + type);
+ message = message.replace("#1", brandName);
+ message = message.replace("#2", Services.appinfo.version);
+ let title = this._bundle.GetStringFromName("sync.update.title")
+ let button = this._bundle.GetStringFromName("sync.update.button")
+ let close = this._bundle.GetStringFromName("sync.update.close")
+
+ let flags = Services.prompt.BUTTON_POS_0 * Services.prompt.BUTTON_TITLE_IS_STRING +
+ Services.prompt.BUTTON_POS_1 * Services.prompt.BUTTON_TITLE_IS_STRING;
+ let choice = Services.prompt.confirmEx(window, title, message, flags, button, close, null, null, {});
+ if (choice == 0)
+ Browser.addTab("https://services.mozilla.com/update/", true, Browser.selectedTab);
+ }
+ }
+
+ device.value = Weave.Service.clientsEngine.localName || "";
+ },
+
+ changeName: function changeName(aInput) {
+ // Make sure to update to a modified name, e.g., empty-string -> default
+ Weave.Service.clientsEngine.localName = aInput.value;
+ aInput.value = Weave.Service.clientsEngine.localName;
+ },
+
+ showMessage: function showMessage(aMsg, aValue, aButtons) {
+ let notification = this._msg.getNotificationWithValue(aValue);
+ if (notification)
+ return;
+
+ this._msg.appendNotification(aMsg, aValue, "", this._msg.PRIORITY_WARNING_LOW, aButtons);
+ },
+
+ _validateServer: function _validateServer(aURL) {
+ let uri = Weave.Utils.makeURI(aURL);
+
+ if (!uri && aURL)
+ uri = Weave.Utils.makeURI("https://" + aURL);
+
+ if (!uri)
+ return "";
+ return uri.spec;
+ },
+
+ openTutorial: function _openTutorial() {
+ WeaveGlue.close();
+
+ let formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].getService(Ci.nsIURLFormatter);
+ let url = formatter.formatURLPref("app.sync.tutorialURL");
+ BrowserUI.newTab(url, Browser.selectedTab);
+ },
+
+ loadSetupData: function _loadSetupData() {
+ this.setupData = {};
+ this.setupData.account = Weave.Service.identity.account || "";
+ this.setupData.password = Weave.Service.identity.basicPassword || "";
+ this.setupData.synckey = Weave.Service.identity.syncKey || "";
+
+ let serverURL = Weave.Service.serverURL;
+ let defaultPrefs = Services.prefs.getDefaultBranch(null);
+ if (serverURL == defaultPrefs.getCharPref("services.sync.serverURL"))
+ serverURL = "";
+ this.setupData.serverURL = serverURL;
+ }
+};
+
+
+const PIN_PART_LENGTH = 4;
+
+let SyncPairDevice = {
+ jpake: null,
+
+ open: function open() {
+ this.code1.setAttribute("maxlength", PIN_PART_LENGTH);
+ this.code2.setAttribute("maxlength", PIN_PART_LENGTH);
+ this.code3.setAttribute("maxlength", PIN_PART_LENGTH);
+ this.nextFocusEl = {code1: this.code2,
+ code2: this.code3,
+ code3: this.connectbutton};
+
+ document.getElementById("syncpair-container").hidden = false;
+ DialogUI.pushDialog(this);
+ this.code1.focus();
+
+ // Kick off a sync. That way the server will have the most recent data from
+ // this computer and it will show up immediately on the new device.
+ Weave.SyncScheduler.scheduleNextSync(0);
+ },
+
+ close: function close() {
+ this.code1.value = this.code2.value = this.code3.value = "";
+ this.code1.disabled = this.code2.disabled = this.code3.disabled = false;
+ this.connectbutton.disabled = true;
+ if (this.jpake) {
+ this.jpake.abort();
+ this.jpake = null;
+ }
+ document.getElementById("syncpair-container").hidden = true;
+ DialogUI.popDialog();
+ },
+
+ onTextBoxInput: function onTextBoxInput(textbox) {
+ if (textbox && textbox.value.length == PIN_PART_LENGTH) {
+ let name = textbox.id.split("-")[1];
+ this.nextFocusEl[name].focus();
+ }
+
+ this.connectbutton.disabled =
+ !(this.code1.value.length == PIN_PART_LENGTH &&
+ this.code2.value.length == PIN_PART_LENGTH &&
+ this.code3.value.length == PIN_PART_LENGTH);
+ },
+
+ connect: function connect() {
+ let self = this;
+ let jpake = this.jpake = new Weave.JPAKEClient({
+ onPaired: function onPaired() {
+ let credentials = {account: Weave.Service.identity.account,
+ password: Weave.Service.identity.basicPassword,
+ synckey: Weave.Service.identity.syncKey,
+ serverURL: Weave.Service.serverURL};
+ jpake.sendAndComplete(credentials);
+ },
+ onComplete: function onComplete() {
+ self.jpake = null;
+ self.close();
+
+ // Schedule a Sync for soonish to fetch the data uploaded by the
+ // device with which we just paired.
+ Weave.SyncScheduler.scheduleNextSync(Weave.SyncScheduler.activeInterval);
+ },
+ onAbort: function onAbort(error) {
+ self.jpake = null;
+
+ // Aborted by user, ignore.
+ if (error == Weave.JPAKE_ERROR_USERABORT) {
+ return;
+ }
+
+ self.code1.value = self.code2.value = self.code3.value = "";
+ self.code1.disabled = self.code2.disabled = self.code3.disabled = false;
+ self.code1.focus();
+ }
+ });
+ this.code1.disabled = this.code2.disabled = this.code3.disabled = true;
+ this.connectbutton.disabled = true;
+
+ let pin = this.code1.value + this.code2.value + this.code3.value;
+ let expectDelay = false;
+ jpake.pairWithPIN(pin, expectDelay);
+ }
+};
+["code1", "code2", "code3", "connectbutton"].forEach(function (id) {
+ XPCOMUtils.defineLazyGetter(SyncPairDevice, id, function() {
+ return document.getElementById("syncpair-" + id);
+ });
+});
diff --git a/browser/metro/base/content/video.js b/browser/metro/base/content/video.js
new file mode 100644
index 000000000000..7f2aecee1ab0
--- /dev/null
+++ b/browser/metro/base/content/video.js
@@ -0,0 +1,66 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var FullScreenVideo = {
+ _tab: null,
+
+ init: function fsv_init() {
+ // These come in from content.js, currently we only use Start.
+ messageManager.addMessageListener("Browser:FullScreenVideo:Start", this.show.bind(this));
+ messageManager.addMessageListener("Browser:FullScreenVideo:Close", this.hide.bind(this));
+ messageManager.addMessageListener("Browser:FullScreenVideo:Play", this.play.bind(this));
+ messageManager.addMessageListener("Browser:FullScreenVideo:Pause", this.pause.bind(this));
+
+ // If the screen supports brightness locks, we will utilize that, see checkBrightnessLocking()
+ try {
+ this.screen = null;
+ let screenManager = Cc["@mozilla.org/gfx/screenmanager;1"].getService(Ci.nsIScreenManager);
+ this.screen = screenManager.primaryScreen;
+ }
+ catch (e) {} // The screen does not support brightness locks
+ },
+
+ play: function() {
+ this.playing = true;
+ this.checkBrightnessLocking();
+ },
+
+ pause: function() {
+ this.playing = false;
+ this.checkBrightnessLocking();
+ },
+
+ checkBrightnessLocking: function() {
+ // screen manager support for metro: bug 776113
+ var shouldLock = !!this.screen && !!window.fullScreen && !!this.playing;
+ var locking = !!this.brightnessLocked;
+ if (shouldLock == locking)
+ return;
+
+ if (shouldLock)
+ this.screen.lockMinimumBrightness(this.screen.BRIGHTNESS_FULL);
+ else
+ this.screen.unlockMinimumBrightness(this.screen.BRIGHTNESS_FULL);
+ this.brightnessLocked = shouldLock;
+ },
+
+ show: function fsv_show() {
+ this.createTab();
+ this.checkBrightnessLocking();
+ },
+
+ hide: function fsv_hide() {
+ this.checkBrightnessLocking();
+ this.destroyTab();
+ },
+
+ createTab: function fsv_createBrowser() {
+ this._tab = BrowserUI.newTab("chrome://browser/content/fullscreen-video.xhtml");
+ },
+
+ destroyTab: function fsv_destroyBrowser() {
+ Browser.closeTab(this._tab);
+ this._tab = null;
+ }
+};
diff --git a/browser/metro/base/jar.mn b/browser/metro/base/jar.mn
new file mode 100644
index 000000000000..28da25150485
--- /dev/null
+++ b/browser/metro/base/jar.mn
@@ -0,0 +1,104 @@
+#filter substitution
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+
+chrome.jar:
+% content browser %content/
+
+ content/aboutCertError.xhtml (content/pages/aboutCertError.xhtml)
+* content/aboutRights.xhtml (content/pages/aboutRights.xhtml)
+ content/aboutCrash.xhtml (content/pages/aboutCrash.xhtml)
+ content/blockedSite.xhtml (content/pages/blockedSite.xhtml)
+ content/fullscreen-video.xhtml (content/pages/fullscreen-video.xhtml)
+ content/netError.xhtml (content/pages/netError.xhtml)
+
+* content/bindings/bindings.xml (content/bindings/bindings.xml)
+ content/bindings/tabs.xml (content/bindings/tabs.xml)
+ content/bindings/toggleswitch.xml (content/bindings/toggleswitch.xml)
+* content/bindings/browser.xml (content/bindings/browser.xml)
+ content/bindings/browser.js (content/bindings/browser.js)
+ content/bindings/downloads.xml (content/bindings/downloads.xml)
+ content/bindings/console.xml (content/bindings/console.xml)
+ content/bindings/dialog.xml (content/bindings/dialog.xml)
+ content/bindings/pageaction.xml (content/bindings/pageaction.xml)
+ content/bindings/arrowbox.xml (content/bindings/arrowbox.xml)
+ content/bindings/grid.xml (content/bindings/grid.xml)
+ content/bindings/autocomplete.xml (content/bindings/autocomplete.xml)
+ content/bindings/appbar.xml (content/bindings/appbar.xml)
+ content/bindings/flyoutpanel.xml (content/bindings/flyoutpanel.xml)
+ content/bindings/selectionoverlay.xml (content/bindings/selectionoverlay.xml)
+
+ content/prompt/CaptureDialog.xul (content/prompt/CaptureDialog.xul)
+ content/prompt/alert.xul (content/prompt/alert.xul)
+ content/prompt/confirm.xul (content/prompt/confirm.xul)
+ content/prompt/prompt.xul (content/prompt/prompt.xul)
+ content/prompt/promptPassword.xul (content/prompt/promptPassword.xul)
+ content/prompt/select.xul (content/prompt/select.xul)
+ content/prompt/prompt.js (content/prompt/prompt.js)
+ content/prompt/share.xul (content/prompt/share.xul)
+ content/prompt/masterPassword.xul (content/prompt/masterPassword.xul)
+ content/prompt/removeMasterPassword.xul (content/prompt/removeMasterPassword.xul)
+
+* content/helperui/AlertsHelper.js (content/helperui/AlertsHelper.js)
+ content/helperui/CaptureDialog.js (content/helperui/CaptureDialog.js)
+ content/helperui/CapturePickerUI.js (content/helperui/CapturePickerUI.js)
+ content/helperui/CharsetMenu.js (content/helperui/CharsetMenu.js)
+ content/helperui/IdentityUI.js (content/helperui/IdentityUI.js)
+ content/helperui/IndexedDB.js (content/helperui/IndexedDB.js)
+ content/helperui/MasterPasswordUI.js (content/helperui/MasterPasswordUI.js)
+ content/helperui/MenuUI.js (content/helperui/MenuUI.js)
+ content/helperui/OfflineApps.js (content/helperui/OfflineApps.js)
+ content/helperui/SelectHelperUI.js (content/helperui/SelectHelperUI.js)
+ content/helperui/SelectionHelperUI.js (content/helperui/SelectionHelperUI.js)
+ content/helperui/SharingUI.js (content/helperui/SharingUI.js)
+ content/helperui/FormHelperUI.js (content/helperui/FormHelperUI.js)
+ content/helperui/FindHelperUI.js (content/helperui/FindHelperUI.js)
+
+ content/contenthandlers/ContextMenuHandler.js (content/contenthandlers/ContextMenuHandler.js)
+ content/contenthandlers/PluginCTPHandler.js (content/contenthandlers/PluginCTPHandler.js)
+ content/contenthandlers/SelectionHandler.js (content/contenthandlers/SelectionHandler.js)
+ content/contenthandlers/FormHelper.js (content/contenthandlers/FormHelper.js)
+ content/contenthandlers/FindHandler.js (content/contenthandlers/FindHandler.js)
+ content/contenthandlers/ViewportHandler.js (content/contenthandlers/ViewportHandler.js)
+ content/contenthandlers/ConsoleAPIObserver.js (content/contenthandlers/ConsoleAPIObserver.js)
+* content/contenthandlers/Content.js (content/contenthandlers/Content.js)
+
+ content/BrowserTouchHandler.js (content/BrowserTouchHandler.js)
+* content/WebProgress.js (content/WebProgress.js)
+ content/config.xul (content/config.xul)
+ content/config.js (content/config.js)
+* content/browser.xul (content/browser.xul)
+* content/browser.js (content/browser.js)
+* content/browser-ui.js (content/browser-ui.js)
+* content/browser-scripts.js (content/browser-scripts.js)
+* content/ContextCommands.js (content/ContextCommands.js)
+* content/PageActions.js (content/PageActions.js)
+ content/commandUtil.js (content/commandUtil.js)
+ content/appbar.js (content/appbar.js)
+ content/shell.xul (content/jsshell/shell.xul)
+ content/shell.html (content/jsshell/shell.html)
+ content/browser.css (content/browser.css)
+ content/cursor.css (content/cursor.css)
+% content branding %content/branding/
+ content/sanitize.js (content/sanitize.js)
+* content/input.js (content/input.js)
+* content/Util.js (content/Util.js)
+ content/bookmarks.js (content/bookmarks.js)
+* content/preferences.js (content/preferences.js)
+ content/exceptions.js (content/exceptions.js)
+* content/downloads.js (content/downloads.js)
+ content/history.js (content/history.js)
+ content/TopSites.js (content/TopSites.js)
+ content/console.js (content/console.js)
+ content/AnimatedZoom.js (content/AnimatedZoom.js)
+ content/LoginManagerChild.js (content/LoginManagerChild.js)
+ content/video.js (content/video.js)
+#ifdef MOZ_SERVICES_SYNC
+ content/sync.js (content/sync.js)
+ content/RemoteTabs.js (content/RemoteTabs.js)
+#endif
+
+% override chrome://global/content/config.xul chrome://browser/content/config.xul
+% override chrome://global/content/netError.xhtml chrome://browser/content/netError.xhtml
diff --git a/browser/metro/base/tests/Makefile.in b/browser/metro/base/tests/Makefile.in
new file mode 100644
index 000000000000..ee15013618ab
--- /dev/null
+++ b/browser/metro/base/tests/Makefile.in
@@ -0,0 +1,29 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+relativesrcdir = @relativesrcdir@
+
+include $(DEPTH)/config/autoconf.mk
+include $(topsrcdir)/config/rules.mk
+
+_BROWSER_FILES = \
+ head.js \
+ browser_test.js \
+ browser_canonizeURL.js \
+ browser_context_ui.js \
+ browser_onscreen_keyboard.js \
+ browser_onscreen_keyboard.html \
+ browser_remotetabs.js \
+ browser_downloads.js \
+ browser_plugin_input.html \
+ browser_plugin_input_mouse.js \
+ browser_plugin_input_keyboard.js \
+ $(NULL)
+
+libs:: $(_BROWSER_FILES)
+ $(INSTALL) $(foreach f,$^,"$f") $(DEPTH)/_tests/testing/mochitest/metro/
diff --git a/browser/metro/base/tests/addons/browser_install1_1/bootstrap.js b/browser/metro/base/tests/addons/browser_install1_1/bootstrap.js
new file mode 100644
index 000000000000..7b86e419a3b3
--- /dev/null
+++ b/browser/metro/base/tests/addons/browser_install1_1/bootstrap.js
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function install(data, reason) {}
+function startup(data, reason) {}
+function shutdown(data, reason) {}
+function uninstall(data, reason) {}
+
diff --git a/browser/metro/base/tests/addons/browser_install1_1/install.rdf b/browser/metro/base/tests/addons/browser_install1_1/install.rdf
new file mode 100644
index 000000000000..0825d11aa0cb
--- /dev/null
+++ b/browser/metro/base/tests/addons/browser_install1_1/install.rdf
@@ -0,0 +1,24 @@
+
+
+
+
+
+ addon1@tests.mozilla.org
+ 1.0
+ http://example.com/browser/mobile/chrome/tests/browser_upgrade.rdf
+ true
+
+
+
+ toolkit@mozilla.org
+ 0
+ *
+
+
+
+
+ Install Tests
+
+
+
diff --git a/browser/metro/base/tests/addons/browser_install1_2/install.rdf b/browser/metro/base/tests/addons/browser_install1_2/install.rdf
new file mode 100644
index 000000000000..945afb22b014
--- /dev/null
+++ b/browser/metro/base/tests/addons/browser_install1_2/install.rdf
@@ -0,0 +1,22 @@
+
+
+
+
+
+ addon2@tests.mozilla.org
+ 2.0
+
+
+
+ toolkit@mozilla.org
+ 0
+ *
+
+
+
+
+ Install Tests 2
+
+
+
diff --git a/browser/metro/base/tests/addons/browser_install1_3/install.rdf b/browser/metro/base/tests/addons/browser_install1_3/install.rdf
new file mode 100644
index 000000000000..2c10f4921628
--- /dev/null
+++ b/browser/metro/base/tests/addons/browser_install1_3/install.rdf
@@ -0,0 +1,23 @@
+
+
+
+
+
+ addon1@tests.mozilla.org
+ 3.0
+ http://example.com/browser/mobile/chrome/tests/browser_upgrade.rdf
+
+
+
+ toolkit@mozilla.org
+ 0
+ *
+
+
+
+
+ Install Tests
+
+
+
diff --git a/browser/metro/base/tests/addons/browser_locale1/boostrap.js b/browser/metro/base/tests/addons/browser_locale1/boostrap.js
new file mode 100644
index 000000000000..7b86e419a3b3
--- /dev/null
+++ b/browser/metro/base/tests/addons/browser_locale1/boostrap.js
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function install(data, reason) {}
+function startup(data, reason) {}
+function shutdown(data, reason) {}
+function uninstall(data, reason) {}
+
diff --git a/browser/metro/base/tests/addons/browser_locale1/chrome.manifest b/browser/metro/base/tests/addons/browser_locale1/chrome.manifest
new file mode 100644
index 000000000000..a909200d5b02
--- /dev/null
+++ b/browser/metro/base/tests/addons/browser_locale1/chrome.manifest
@@ -0,0 +1,4 @@
+locale mozapps te-st chrome # locale
+locale browser te-st chrome # duplicate locale
+locale browser te-st-a chrome # second locale
+locale branding te-st-3 chrome # wrong component
diff --git a/browser/metro/base/tests/addons/browser_locale1/install.rdf b/browser/metro/base/tests/addons/browser_locale1/install.rdf
new file mode 100644
index 000000000000..ba92a4581869
--- /dev/null
+++ b/browser/metro/base/tests/addons/browser_locale1/install.rdf
@@ -0,0 +1,24 @@
+
+
+
+
+
+ locale1@tests.mozilla.org
+ 1.0
+ 8
+ true
+
+
+
+ toolkit@mozilla.org
+ 0
+ *
+
+
+
+
+ Test Locale
+
+
+
diff --git a/browser/metro/base/tests/browser_canonizeURL.js b/browser/metro/base/tests/browser_canonizeURL.js
new file mode 100644
index 000000000000..1528a78e8c03
--- /dev/null
+++ b/browser/metro/base/tests/browser_canonizeURL.js
@@ -0,0 +1,21 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function test() {
+ let testcases = [
+ ["example", {}, "example"],
+ ["example", {ctrlKey: true}, "http://www.example.com/"],
+ ["example.org", {ctrlKey: true}, "example.org"],
+ ["example", {shiftKey: true}, "http://www.example.net/"],
+ ["example", {shiftKey: true, ctrlKey: true}, "http://www.example.org/"],
+ [" example ", {ctrlKey: true}, "http://www.example.com/"],
+ [" example/a ", {ctrlKey: true}, "http://www.example.com/a"]
+ ];
+ for (let [input, modifiers, result] of testcases) {
+ is(BrowserUI._canonizeURL(input, modifiers), result, input + " -> " + result);
+ }
+}
diff --git a/browser/metro/base/tests/browser_context_ui.js b/browser/metro/base/tests/browser_context_ui.js
new file mode 100644
index 000000000000..6b3700c075a3
--- /dev/null
+++ b/browser/metro/base/tests/browser_context_ui.js
@@ -0,0 +1,93 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+function test() {
+ runTests();
+}
+
+gTests.push({
+ desc: "Context UI on about:start",
+ run: function testAboutStart() {
+ yield addTab("about:start");
+ is(StartUI.isVisible, true, "Start UI is displayed on about:start");
+ is(ContextUI.isVisible, true, "Toolbar is displayed on about:start");
+ is(ContextUI.isExpanded, false, "Tab bar is not displayed initially");
+ is(Elements.appbar.isShowing, false, "Appbar is not displayed initially");
+
+ // toggle on
+ doEdgeUIGesture();
+ is(ContextUI.isVisible, true, "Toolbar is still visible after one swipe");
+ is(ContextUI.isExpanded, true, "Tab bar is visible after one swipe");
+ is(Elements.appbar.isShowing, true, "Appbar is visible after one swipe");
+
+ // toggle off
+ doEdgeUIGesture();
+ is(ContextUI.isVisible, true, "Toolbar is still visible after second swipe");
+ is(ContextUI.isExpanded, false, "Tab bar is hidden after second swipe");
+ is(Elements.appbar.isShowing, false, "Appbar is hidden after second swipe");
+
+ // sanity check - toggle on again
+ doEdgeUIGesture();
+ is(ContextUI.isVisible, true, "Toolbar is still visible after third swipe");
+ is(ContextUI.isExpanded, true, "Tab bar is visible after third swipe");
+ is(Elements.appbar.isShowing, true, "Appbar is visible after third swipe");
+
+ is(StartUI.isVisible, true, "Start UI is still visible");
+ }
+});
+
+gTests.push({
+ desc: "Context UI on a web page (about:)",
+ run: function testAbout() {
+ yield addTab("about:");
+ ContextUI.dismiss();
+ is(StartUI.isVisible, false, "Start UI is not visible on about:");
+ is(ContextUI.isVisible, false, "Toolbar is not initially visible on about:");
+ is(ContextUI.isExpanded, false, "Tab bar is not initially visible on about:");
+ is(Elements.appbar.isShowing, false, "Appbar is not initially visible on about on about::");
+
+ doEdgeUIGesture();
+ is(ContextUI.isVisible, true, "Toolbar is visible after one swipe");
+ is(ContextUI.isExpanded, true, "Tab bar is visble after one swipe");
+ is(Elements.appbar.isShowing, true, "Appbar is visible after one swipe");
+
+ doEdgeUIGesture();
+ is(ContextUI.isVisible, false, "Toolbar is not visible after second swipe");
+ is(ContextUI.isExpanded, false, "Tab bar is not visible after second swipe");
+ is(Elements.appbar.isShowing, false, "Appbar is hidden after second swipe");
+
+ is(StartUI.isVisible, false, "Start UI is still not visible");
+ }
+});
+
+gTests.push({
+ desc: "Control-L keyboard shortcut",
+ run: function testAbout() {
+ let tab = yield addTab("about:");
+ ContextUI.dismiss();
+ is(ContextUI.isVisible, false, "Navbar is not initially visible");
+ is(ContextUI.isExpanded, false, "Tab bar is not initially visible");
+
+ EventUtils.synthesizeKey('l', { accelKey: true });
+ is(ContextUI.isVisible, true, "Navbar is visible");
+ is(ContextUI.isExpanded, false, "Tab bar is not visible");
+
+ let edit = document.getElementById("urlbar-edit");
+ is(edit.value, "about:", "Location field contains the page URL");
+ ok(document.commandDispatcher.focusedElement, edit.inputField, "Location field is focused");
+ is(edit.selectionStart, 0, "Location field is selected");
+ is(edit.selectionEnd, edit.value.length, "Location field is selected");
+
+ Browser.closeTab(tab);
+ }
+});
+
+function doEdgeUIGesture() {
+ let event = document.createEvent("Events");
+ event.initEvent("MozEdgeUIGesture", true, false);
+ window.dispatchEvent(event);
+}
diff --git a/browser/metro/base/tests/browser_downloads.js b/browser/metro/base/tests/browser_downloads.js
new file mode 100644
index 000000000000..2c0777040bf5
--- /dev/null
+++ b/browser/metro/base/tests/browser_downloads.js
@@ -0,0 +1,450 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/**
+ * Provides infrastructure for automated download components tests.
+ * (adapted from browser/component/downloads test's head.js)
+ */
+
+////////////////////////////////////////////////////////////////////////////////
+//// Globals
+
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+
+const nsIDM = Ci.nsIDownloadManager;
+
+////////////////////////////////////////////////////////////////////////////////
+// Test Helpers
+
+var { spawn } = Task;
+
+function equalStrings(){
+ let ref = ""+arguments[0];
+ for (let i=1; i= 0; i--) {
+ let dataRow = aDataRows[i];
+ let download = yield addDownloadRow(dataRow);
+
+ // At each iteration, ensure that the start and end time in the global
+ // template is distinct, as these column are used to sort each download
+ // in its category.
+ gDownloadRowTemplate.startTime++;
+ gDownloadRowTemplate.endTime++;
+ }
+ } finally {
+ info("gen_addDownloadRows, finally");
+ }
+}
+
+/////////////////////////////////////
+// Test implementations
+
+/**
+ * Test that:
+ * view can represent all possible download states
+ * clearDownloads does in fact empty the view
+ * addDownload adds an item, to the right place
+ * removeDownload removes an item, leaving the view in the correct state
+ */
+gTests.push({
+ desc: "UI sanity check",
+ run: function(){
+
+ ok(document.getElementById('downloads-list'), "Downloads panel grid is present");
+ ok(DownloadsPanelView, "Downloads panel object is present");
+
+ PanelUI.show('downloads-container');
+ ok(DownloadsPanelView.visible, "Downloads panel is visible after being shown");
+ }
+});
+
+gTests.push({
+ desc: "zero downloads",
+ run: function () {
+
+ yield resetDownloads();
+
+ let downloadslist = document.getElementById("downloads-list");
+
+ // wait for the richgrid to announce its readiness
+ // .. fail a test if the timeout is exceeded
+ let isReady = waitForEvent(downloadslist, "DownloadsReady", 2000);
+ // tickle the view to cause it to refresh itself
+ DownloadsPanelView._view.getDownloads();
+
+ yield isReady;
+
+ if (!isReady || isReady instanceof Error){
+ ok(false, "DownloadsReady event never fired");
+ }
+
+ let count = downloadslist.children.length;
+ is(count, 0, "Zero items in grid view with empty downloads db");
+ }
+});
+
+/**
+ * Make sure the downloads panel can display items in the right order and
+ * contains the expected data.
+ */
+gTests.push({
+ desc: "Show downloads",
+ run: function(){
+ // Display one of each download state.
+ let DownloadData = [
+ { endTime: 1180493839859239, state: nsIDM.DOWNLOAD_NOTSTARTED },
+ { endTime: 1180493839859238, state: nsIDM.DOWNLOAD_DOWNLOADING },
+ { endTime: 1180493839859237, state: nsIDM.DOWNLOAD_PAUSED },
+ { endTime: 1180493839859236, state: nsIDM.DOWNLOAD_SCANNING },
+ { endTime: 1180493839859235, state: nsIDM.DOWNLOAD_QUEUED },
+ { endTime: 1180493839859234, state: nsIDM.DOWNLOAD_FINISHED },
+ { endTime: 1180493839859233, state: nsIDM.DOWNLOAD_FAILED },
+ { endTime: 1180493839859232, state: nsIDM.DOWNLOAD_CANCELED },
+ { endTime: 1180493839859231, state: nsIDM.DOWNLOAD_BLOCKED_PARENTAL },
+ { endTime: 1180493839859230, state: nsIDM.DOWNLOAD_DIRTY },
+ { endTime: 1180493839859229, state: nsIDM.DOWNLOAD_BLOCKED_POLICY },
+ ];
+
+ yield resetDownloads();
+ DownloadsPanelView._view.getDownloads();
+
+ // NB: beware display limits which might cause mismatch btw. rendered item and db rows
+
+ try {
+ // Populate the downloads database with the data required by this test.
+ // we're going to add stuff to the downloads db.
+ yield spawn( gen_addDownloadRows( DownloadData ) );
+
+ // Test item data and count. This also tests the ordering of the display.
+ let downloadslist = document.getElementById("downloads-list");
+ // wait for the richgrid to announce its readiness
+ // .. fail a test if the timeout is exceeded
+ let isReady = waitForEvent(downloadslist, "DownloadsReady", 2000);
+
+ // tickle the view to cause it to refresh itself
+ DownloadsPanelView._view.getDownloads();
+
+ yield isReady;
+
+ if (!isReady || isReady instanceof Error){
+ ok(false, "DownloadsReady event never fired");
+ }
+
+ is(downloadslist.children.length, DownloadData.length,
+ "There is the correct number of richlistitems");
+
+ for (let i = 0; i < downloadslist.children.length; i++) {
+ let element = downloadslist.children[i];
+ let id = element.getAttribute("downloadId");
+ let dataItem = Downloads.manager.getDownload(id); // nsIDownload object
+
+ ok( equalStrings(
+ element.getAttribute("name"),
+ dataItem.displayName,
+ DownloadData[i].name
+ ), "Download names match up");
+
+ ok( equalNumbers(
+ element.getAttribute("state"),
+ dataItem.state,
+ DownloadData[i].state
+ ), "Download states match up");
+
+ ok( equalStrings(
+ element.getAttribute("target"),
+ dataItem.target.spec,
+ DownloadData[i].target
+ ), "Download targets match up");
+
+ if (DownloadData[i].source && dataItem.referrer){
+ ok( equalStrings(
+ dataItem.referrer.spec,
+ DownloadData[i].source
+ ), "Download sources match up");
+ }
+ }
+ } catch(e) {
+ info("Show downloads, some error: " + e);
+ }
+ finally {
+ // Clean up when the test finishes.
+ DownloadsPanelView._view.clearDownloads();
+ yield resetDownloads();
+ }
+ }
+});
+
+/**
+ * Make sure the downloads can be removed with the expected result on the panel and its listing
+ */
+gTests.push({
+ desc: "Remove downloads",
+ run: function(){
+ // Push a few items into the downloads db.
+ let DownloadData = [
+ { endTime: 1180493839859239, state: nsIDM.DOWNLOAD_FINISHED },
+ { endTime: 1180493839859238, state: nsIDM.DOWNLOAD_FINISHED },
+ { endTime: 1180493839859237, state: nsIDM.DOWNLOAD_FINISHED }
+ ];
+
+ yield resetDownloads();
+ DownloadsPanelView._view.getDownloads();
+
+ try {
+ // Populate the downloads database with the data required by this test.
+ yield spawn( gen_addDownloadRows( DownloadData ) );
+
+ // Test item data and count. This also tests the ordering of the display.
+ let downloadslist = document.getElementById("downloads-list");
+ // wait for the richgrid to announce its readiness
+ // .. fail a test if the timeout is exceeded
+ let isReady = waitForEvent(downloadslist, "DownloadsReady", 2000);
+ // tickle the view to cause it to refresh itself
+ DownloadsPanelView._view.getDownloads();
+
+ yield isReady;
+
+ if (!isReady || isReady instanceof Error){
+ is(false, "DownloadsReady event never fired");
+ }
+
+ let downloadRows = null,
+ promisedDownloads;
+ // get all the downloads from the db
+ promisedDownloads = getPromisedDbResult(
+ "SELECT guid "
+ + "FROM moz_downloads "
+ + "ORDER BY startTime DESC"
+ ).then(function(aRows){
+ downloadRows = aRows;
+ }, function(aError){
+ throw aError;
+ });
+ yield promisedDownloads;
+
+ is(downloadRows.length, 3, "Correct number of downloads in the db before removal");
+
+ // remove the first one
+ let itemNode = downloadslist.children[0];
+ let id = itemNode.getAttribute("downloadId");
+ // check the file exists
+ let download = Downloads.manager.getDownload( id );
+ let file = download.targetFile;
+ ok(file && file.exists());
+
+ Downloads.manager.removeDownload( id );
+ // remove is async(?), wait a bit
+ yield waitForMs(0);
+
+ // get all the downloads from the db
+ downloadRows = null;
+ promisedDownloads = getPromisedDbResult(
+ "SELECT guid "
+ + "FROM moz_downloads "
+ + "ORDER BY startTime DESC"
+ ).then(function(aRows){
+ downloadRows = aRows;
+ }, function(aError){
+ throw aError;
+ });
+ yield promisedDownloads;
+
+ is(downloadRows.length, 2, "Correct number of downloads in the db after removal");
+
+ is(2, downloadslist.children.length,
+ "Removing a download updates the items list");
+ ok(file && file.exists(), "File still exists after download removal");
+
+ } catch(e) {
+ info("Remove downloads, some error: " + e);
+ }
+ finally {
+ // Clean up when the test finishes.
+ DownloadsPanelView._view.clearDownloads();
+ yield resetDownloads();
+ }
+ }
+});
+
+
diff --git a/browser/metro/base/tests/browser_onscreen_keyboard.html b/browser/metro/base/tests/browser_onscreen_keyboard.html
new file mode 100644
index 000000000000..26e7b4f4b8d9
--- /dev/null
+++ b/browser/metro/base/tests/browser_onscreen_keyboard.html
@@ -0,0 +1,12 @@
+
+
+
+ On-Screen Keyboard Test
+
+
+
+
+
+
diff --git a/browser/metro/base/tests/browser_onscreen_keyboard.js b/browser/metro/base/tests/browser_onscreen_keyboard.js
new file mode 100644
index 000000000000..4f3a94434501
--- /dev/null
+++ b/browser/metro/base/tests/browser_onscreen_keyboard.js
@@ -0,0 +1,54 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ runTests();
+}
+
+gTests.push({
+ desc: "Onscreen keyboard tests",
+ run: function() {
+ // By design, Metro apps can't show the keyboard programmatically, so we
+ // can't use the real keyboard in this test:
+ // http://msdn.microsoft.com/en-us/library/windows/apps/hh465404.aspx#user-driven_invocation
+ //
+ // Instead, we will use this mock object to simulate keyboard changes.
+ let originalUtils = window.MetroUtils;
+ window.MetroUtils = {
+ keyboardHeight: 0,
+ keyboardVisible: false
+ };
+ registerCleanupFunction(function() {
+ window.MetroUtils = originalUtils;
+ });
+
+ let tab = yield addTab(chromeRoot + "browser_onscreen_keyboard.html");
+ // Explicitly dismiss the toolbar at the start, because it messes with the
+ // keyboard and sizing if it's dismissed later.
+ ContextUI.dismiss();
+
+ let doc = tab.browser.contentDocument;
+ let text = doc.getElementById("text")
+ let rect0 = text.getBoundingClientRect();
+ text.focus();
+
+ // "Show" the keyboard.
+ MetroUtils.keyboardHeight = 100;
+ MetroUtils.keyboardVisible = true;
+ Services.obs.notifyObservers(null, "metro_softkeyboard_shown", null);
+
+ let rect1 = text.getBoundingClientRect();
+ is(rect1.top, rect0.top - 100, "text field moves up by 100px");
+
+ // "Hide" the keyboard.
+ MetroUtils.keyboardHeight = 0;
+ MetroUtils.keyboardVisible = false;
+ Services.obs.notifyObservers(null, "metro_softkeyboard_hidden", null);
+
+ let rect2 = text.getBoundingClientRect();
+ is(rect2.top, rect0.top, "text field moves back to the original position");
+
+ finish();
+ }
+});
diff --git a/browser/metro/base/tests/browser_plugin_input.html b/browser/metro/base/tests/browser_plugin_input.html
new file mode 100644
index 000000000000..80b5cd109179
--- /dev/null
+++ b/browser/metro/base/tests/browser_plugin_input.html
@@ -0,0 +1,8 @@
+
+
+ Test Plugin Input
+
+
+
+
+
diff --git a/browser/metro/base/tests/browser_plugin_input_keyboard.js b/browser/metro/base/tests/browser_plugin_input_keyboard.js
new file mode 100644
index 000000000000..023cdefb959e
--- /dev/null
+++ b/browser/metro/base/tests/browser_plugin_input_keyboard.js
@@ -0,0 +1,53 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ runTests();
+}
+
+gTests.push({
+ desc: "Plugin keyboard input",
+ run: function() {
+ Services.prefs.setBoolPref("plugin.disable", false);
+ Services.prefs.setBoolPref("plugins.click_to_play", false);
+ registerCleanupFunction(Services.prefs.clearUserPref.bind(null, "plugin.disable"));
+ registerCleanupFunction(Services.prefs.clearUserPref.bind(null, "plugins.click_to_play"));
+
+ let tab = yield addTab(chromeRoot + "browser_plugin_input.html");
+
+ let doc = tab.browser.contentDocument;
+ let plugin = doc.getElementById("plugin1");
+ let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+ ok(objLoadingContent.activated, "Plugin activated");
+ plugin.focus();
+
+ try {
+ is(plugin.getLastKeyText(), "", "Plugin should not have received "
+ + "any character events yet.");
+ } catch(e) {
+ ok(false, "plugin.getLastKeyText should not throw: " + e);
+ }
+
+ let keys = [{ kbLayout: arSpanish,
+ keyCode: 65,
+ modifiers: 0,
+ expectedChar: 'a' }];
+
+ /* XXX: Re-enable this once bug 837293 is fixed
+ { kbLayout: arSpanish,
+ keyCode: 65,
+ modifiers: rightAlt,
+ expectedChar: "á" }];
+ */
+
+ while (keys.length > 0) {
+ let key = keys.shift();
+ info("Sending keypress: " + key.expectedChar);
+ synthesizeNativeKey(key.kbLayout, key.keyCode, key.modifiers);
+ let success = yield waitForCondition(function() plugin.getLastKeyText() == key.expectedChar);
+ ok(success && !(success instanceof Error),
+ "Plugin received char: " + key.expectedChar);
+ }
+ }
+});
diff --git a/browser/metro/base/tests/browser_plugin_input_mouse.js b/browser/metro/base/tests/browser_plugin_input_mouse.js
new file mode 100644
index 000000000000..388b084e2747
--- /dev/null
+++ b/browser/metro/base/tests/browser_plugin_input_mouse.js
@@ -0,0 +1,69 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+function test() {
+ runTests();
+}
+
+gTests.push({
+ desc: "Plugin mouse input",
+ run: function() {
+ // This test needs "Switch primary and secondary buttons" disabled.
+ let origValue = MetroUtils.swapMouseButton(false);
+ registerCleanupFunction(function() MetroUtils.swapMouseButton(origValue));
+
+ Services.prefs.setBoolPref("plugin.disable", false);
+ Services.prefs.setBoolPref("plugins.click_to_play", false);
+ registerCleanupFunction(Services.prefs.clearUserPref.bind(null, "plugin.disable"));
+ registerCleanupFunction(Services.prefs.clearUserPref.bind(null, "plugins.click_to_play"));
+
+ let tab = yield addTab(chromeRoot + "browser_plugin_input.html");
+
+ let doc = tab.browser.contentDocument;
+ let plugin = doc.getElementById("plugin1");
+ let objLoadingContent = plugin.QueryInterface(Ci.nsIObjectLoadingContent);
+ ok(objLoadingContent.activated, "Plugin activated");
+
+ // XXX: This shouldn't be necessary, but removing it causes the first click to
+ // sometimes not register
+ let wait = yield waitForMs(0);
+ ok(wait, "Initial wait");
+
+ try {
+ is(plugin.getMouseUpEventCount(), 0, "Plugin should not have received "
+ + "any mouse up events yet.");
+ } catch(e) {
+ ok(false, "plugin.getMouseUpEventCount should not throw: " + e);
+ }
+
+ let bottom = plugin.getBoundingClientRect().height - 1;
+ let right = plugin.getBoundingClientRect().width - 1;
+ let middleX = right / 2;
+ let middleY = bottom / 2;
+ let left = 1;
+ let top = 1;
+
+ let clicks = [{ x: left, y: top}, // left top corner
+ { x: left, y: middleY}, // left middle
+ { x: left, y: bottom}, // left bottom corner
+ { x: middleX, y: bottom}, // bottom middle
+ { x: right, y: bottom}, // right bottom corner
+ { x: right, y: middleY}, // right middle
+ { x: right, y: top}, // right top corner
+ { x: middleX, y: top}, // top middle
+ { x: middleX, y: middleY}]; // middle
+
+ let curClicks = 0;
+ while (clicks.length > 0) {
+ let click = clicks.shift();
+ curClicks++;
+ info("Sending click " + curClicks + " { x: " + click.x + ", y: " + click.y + "}");
+ synthesizeNativeMouseLDown(plugin, click.x, click.y);
+ synthesizeNativeMouseLUp(plugin, click.x, click.y);
+ let success = yield waitForCondition(function() plugin.getMouseUpEventCount() == curClicks);
+ ok(success && !(success instanceof Error),
+ "Plugin received click " + curClicks);
+ }
+ }
+});
diff --git a/browser/metro/base/tests/browser_remotetabs.js b/browser/metro/base/tests/browser_remotetabs.js
new file mode 100644
index 000000000000..ca9c8d85a6c5
--- /dev/null
+++ b/browser/metro/base/tests/browser_remotetabs.js
@@ -0,0 +1,47 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+"use strict";
+
+////////////////////////////////////////////////////////////////////////////////
+//// Globals
+
+Components.utils.import("resource://services-sync/main.js");
+////////////////////////////////////////////////////////////////////////////////
+//// Test(s)
+
+function test() {
+ is(Weave.Status.checkSetup(), Weave.CLIENT_NOT_CONFIGURED, "Sync should be disabled on start");
+ // check start page is hidden
+
+ let vbox = document.getElementById("start-remotetabs");
+ ok(vbox.hidden, "remote tabs in the start page should be hidden when sync is not enabled");
+ // check container link is hidden
+ let menulink = document.getElementById("menuitem-remotetabs");
+ ok(menulink.hidden, "link to container should be hidden when sync is not enabled");
+
+ // hacky-fake sync setup and enabled. Note the Sync Tracker will spit
+ // a number of warnings about undefined ids
+ Weave.Status._authManager.username = "jane doe"; // must set username before key
+ Weave.Status._authManager.basicPassword = "goatcheesesalad";
+ Weave.Status._authManager.syncKey = "a-bcdef-abcde-acbde-acbde-acbde";
+ // check that it worked
+ isnot(Weave.Status.checkSetup(), Weave.CLIENT_NOT_CONFIGURED, "Sync is enabled");
+ Weave.Svc.Obs.notify("weave:service:setup-complete");
+
+ // start page grid should be visible
+ ok(vbox, "remote tabs grid is present on start page");
+ //PanelUI.show("remotetabs-container");
+ is(vbox.hidden, false, "remote tabs should be visible in start page when sync is enabled");
+ // container link should be visible
+ is(menulink.hidden, false, "link to container should be visible when sync is enabled");
+
+ // hacky-fake sync disable
+ Weave.Status._authManager.deleteSyncCredentials();
+ Weave.Svc.Obs.notify("weave:service:start-over");
+ is(Weave.Status.checkSetup(), Weave.CLIENT_NOT_CONFIGURED, "Sync has been disabled");
+ ok(vbox.hidden, "remote tabs in the start page should be hidden when sync is not enabled");
+ ok(menulink.hidden, "link to container should be hidden when sync is not enabled");
+
+}
diff --git a/browser/metro/base/tests/browser_test.js b/browser/metro/base/tests/browser_test.js
new file mode 100644
index 000000000000..f1d11b7faf48
--- /dev/null
+++ b/browser/metro/base/tests/browser_test.js
@@ -0,0 +1,30 @@
+// Tests for the test functions in head.js
+
+function test() {
+ waitForExplicitFinish();
+ runTests();
+}
+
+gTests.push({
+ desc: "task sanity check",
+ run: function() {
+ let sum2plus2 = yield asyncSum(2, 2);
+ ok(sum2plus2 == 4, "asyncSum responded 2+2=4");
+
+ function asyncSum(a, b) {
+ var defd = Promise.defer();
+ setTimeout(function(){
+ defd.resolve(a+b);
+ }, 25);
+ return defd.promise;
+ }
+ }
+});
+
+gTests.push({
+ desc: "addTab",
+ run: function testAddTab() {
+ let tab = yield addTab("http://example.com/");
+ is(tab, Browser.selectedTab, "The new tab is selected");
+ }
+});
diff --git a/browser/metro/base/tests/head.js b/browser/metro/base/tests/head.js
new file mode 100644
index 000000000000..8a052fbeb4a9
--- /dev/null
+++ b/browser/metro/base/tests/head.js
@@ -0,0 +1,266 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*=============================================================================
+ Globals
+=============================================================================*/
+XPCOMUtils.defineLazyModuleGetter(this, "Promise", "resource://gre/modules/commonjs/sdk/core/promise.js");
+XPCOMUtils.defineLazyModuleGetter(this, "Task", "resource://gre/modules/Task.jsm");
+
+/*=============================================================================
+ Useful constants
+=============================================================================*/
+const serverRoot = "http://example.com/browser/metro/";
+const baseURI = "http://mochi.test:8888/browser/metro/";
+const chromeRoot = getRootDirectory(gTestPath);
+const kDefaultWait = 10000;
+const kDefaultInterval = 50;
+
+/*=============================================================================
+ Asynchronous test helpers
+=============================================================================*/
+/**
+ * Loads a URL in a new tab asynchronously.
+ *
+ * Usage:
+ * Task.spawn(function() {
+ * let tab = yield addTab("http://example.com/");
+ * ok(Browser.selectedTab == tab, "the new tab is selected");
+ * });
+ *
+ * @param aUrl the URL to load
+ * @returns a task that resolves to the new tab object after the URL is loaded.
+ */
+function addTab(aUrl) {
+ return Task.spawn(function() {
+ info("Opening "+aUrl+" in a new tab");
+ let tab = Browser.addTab(aUrl, true);
+ yield waitForEvent(tab.browser, "pageshow");
+
+ is(tab.browser.currentURI.spec, aUrl, aUrl + " is loaded");
+ registerCleanupFunction(function() Browser.closeTab(tab));
+ throw new Task.Result(tab);
+ });
+}
+
+/**
+ * Waits a specified number of miliseconds for a specified event to be
+ * fired on a specified element.
+ *
+ * Usage:
+ * let receivedEvent = waitForEvent(element, "eventName");
+ * // Do some processing here that will cause the event to be fired
+ * // ...
+ * // Now yield until the Promise is fulfilled
+ * yield receivedEvent;
+ * if (receivedEvent && !(receivedEvent instanceof Error)) {
+ * receivedEvent.msg == "eventName";
+ * // ...
+ * }
+ *
+ * @param aSubject the element that should receive the event
+ * @param aEventName the event to wait for
+ * @param aTimeoutMs the number of miliseconds to wait before giving up
+ * @returns a Promise that resolves to the received event, or to an Error
+ */
+function waitForEvent(aSubject, aEventName, aTimeoutMs) {
+ let eventDeferred = Promise.defer();
+ let timeoutMs = aTimeoutMs || kDefaultWait;
+ let timerID = setTimeout(function wfe_canceller() {
+ aSubject.removeEventListener(aEventName, onEvent);
+ eventDeferred.reject( new Error(aEventName+" event timeout") );
+ }, timeoutMs);
+
+ function onEvent(aEvent) {
+ // stop the timeout clock and resume
+ clearTimeout(timerID);
+ eventDeferred.resolve(aEvent);
+ }
+
+ function cleanup() {
+ // unhook listener in case of success or failure
+ aSubject.removeEventListener(aEventName, onEvent);
+ }
+ eventDeferred.promise.then(cleanup, cleanup);
+
+ aSubject.addEventListener(aEventName, onEvent, false);
+ return eventDeferred.promise;
+}
+
+/**
+ * Waits a specified number of miliseconds.
+ *
+ * Usage:
+ * let wait = yield waitForMs(2000);
+ * ok(wait, "2 seconds should now have elapsed");
+ *
+ * @param aMs the number of miliseconds to wait for
+ * @returns a Promise that resolves to true after the time has elapsed
+ */
+function waitForMs(aMs) {
+ info("Wating for " + aMs + "ms");
+ let deferred = Promise.defer();
+ let startTime = Date.now();
+ setTimeout(done, aMs);
+
+ function done() {
+ deferred.resolve(true);
+ info("waitForMs finished waiting, waited for "
+ + (Date.now() - startTime)
+ + "ms");
+ }
+
+ return deferred.promise;
+}
+
+/**
+ * Waits a specified number of miliseconds for a supplied callback to
+ * return a truthy value.
+ *
+ * Usage:
+ * let success = yield waitForCondition(myTestFunction);
+ * if (success && !(success instanceof Error)) {
+ * // ...
+ * }
+ *
+ * @param aCondition the callback that must return a truthy value
+ * @param aTimeoutMs the number of miliseconds to wait before giving up
+ * @param aIntervalMs the number of miliseconds between calls to aCondition
+ * @returns a Promise that resolves to true, or to an Error
+ */
+function waitForCondition(aCondition, aTimeoutMs, aIntervalMs) {
+ let deferred = Promise.defer();
+ let timeoutMs = aTimeoutMs || kDefaultWait;
+ let intervalMs = aIntervalMs || kDefaultInterval;
+ let startTime = Date.now();
+
+ function testCondition() {
+ let now = Date.now();
+ if((now - startTime) > timeoutMs) {
+ deferred.reject( new Error("Timed out waiting for condition to be true") );
+ return;
+ }
+
+ let condition;
+ try {
+ condition = aCondition();
+ } catch (e) {
+ deferred.reject( new Error("Got exception while attempting to test conditino: " + e) );
+ return;
+ }
+
+ if (condition) {
+ deferred.resolve(true);
+ } else {
+ setTimeout(testCondition, intervalMs);
+ }
+ }
+
+ setTimeout(testCondition, 0);
+ return deferred.promise;
+}
+
+/*=============================================================================
+ Native input synthesis helpers
+=============================================================================*/
+// Keyboard layouts for use with synthesizeNativeKey
+const usEnglish = 0x409;
+const arSpanish = 0x2C0A;
+
+// Modifiers for use with synthesizeNativeKey
+const leftShift = 0x100;
+const rightShift = 0x200;
+const leftControl = 0x400;
+const rightControl = 0x800;
+const leftAlt = 0x1000;
+const rightAlt = 0x2000;
+
+function synthesizeNativeKey(aKbLayout, aVKey, aModifiers) {
+ Browser.windowUtils.sendNativeKeyEvent(aKbLayout, aVKey, aModifiers, '', '');
+}
+
+function synthesizeNativeMouse(aElement, aOffsetX, aOffsetY, aMsg) {
+ let x = aOffsetX;
+ let y = aOffsetY;
+ if (aElement) {
+ if (aElement.getBoundingClientRect) {
+ let rect = aElement.getBoundingClientRect();
+ x += rect.left;
+ y += rect.top;
+ } else if(aElement.left && aElement.top) {
+ x += aElement.left;
+ y += aElement.top;
+ }
+ }
+ Browser.windowUtils.sendNativeMouseEvent(x, y, aMsg, 0, null);
+}
+
+function synthesizeNativeMouseMove(aElement, aOffsetX, aOffsetY) {
+ synthesizeNativeMouse(aElement,
+ aOffsetX,
+ aOffsetY,
+ 0x0001); // MOUSEEVENTF_MOVE
+}
+
+function synthesizeNativeMouseLDown(aElement, aOffsetX, aOffsetY) {
+ synthesizeNativeMouse(aElement,
+ aOffsetX,
+ aOffsetY,
+ 0x0002); // MOUSEEVENTF_LEFTDOWN
+}
+
+function synthesizeNativeMouseLUp(aElement, aOffsetX, aOffsetY) {
+ synthesizeNativeMouse(aElement,
+ aOffsetX,
+ aOffsetY,
+ 0x0004); // MOUSEEVENTF_LEFTUP
+}
+
+function synthesizeNativeMouseRDown(aElement, aOffsetX, aOffsetY) {
+ synthesizeNativeMouse(aElement,
+ aOffsetX,
+ aOffsetY,
+ 0x0008); // MOUSEEVENTF_RIGHTDOWN
+}
+
+function synthesizeNativeMouseRUp(aElement, aOffsetX, aOffsetY) {
+ synthesizeNativeMouse(aElement,
+ aOffsetX,
+ aOffsetY,
+ 0x0010); // MOUSEEVENTF_RIGHTUP
+}
+
+function synthesizeNativeMouseMDown(aElement, aOffsetX, aOffsetY) {
+ synthesizeNativeMouse(aElement,
+ aOffsetX,
+ aOffsetY,
+ 0x0020); // MOUSEEVENTF_MIDDLEDOWN
+}
+
+function synthesizeNativeMouseMUp(aElement, aOffsetX, aOffsetY) {
+ synthesizeNativeMouse(aElement,
+ aOffsetX,
+ aOffsetY,
+ 0x0040); // MOUSEEVENTF_MIDDLEUP
+}
+
+/*=============================================================================
+ Test-running helpers
+=============================================================================*/
+let gCurrentTest = null;
+let gTests = [];
+
+function runTests() {
+ waitForExplicitFinish();
+ Task.spawn(function() {
+ while((gCurrentTest = gTests.shift())){
+ info(gCurrentTest.desc);
+ yield Task.spawn(gCurrentTest.run);
+ info("END "+gCurrentTest.desc);
+ }
+ info("done with gTests while loop, calling finish");
+ finish();
+ });
+}
diff --git a/browser/metro/components/AboutRedirector.js b/browser/metro/components/AboutRedirector.js
new file mode 100644
index 000000000000..3b872a7c377e
--- /dev/null
+++ b/browser/metro/components/AboutRedirector.js
@@ -0,0 +1,86 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+let modules = {
+ start: {
+ uri: "about:blank",
+ privileged: false
+ },
+ // about:blank has some bad loading behavior we can avoid, if we use an alias
+ empty: {
+ uri: "about:blank",
+ privileged: false
+ },
+ firstrun: {
+ uri: "chrome://browser/content/firstrun/firstrun.xhtml",
+ privileged: true
+ },
+ rights: {
+#ifdef MOZ_OFFICIAL_BRANDING
+ uri: "chrome://browser/content/aboutRights.xhtml",
+#else
+ uri: "chrome://global/content/aboutRights-unbranded.xhtml",
+#endif
+ privileged: false
+ },
+ blocked: {
+ uri: "chrome://browser/content/blockedSite.xhtml",
+ privileged: true
+ },
+ certerror: {
+ uri: "chrome://browser/content/aboutCertError.xhtml",
+ privileged: true
+ },
+ // an alias for about:start
+ home: {
+ uri: "about:blank",
+ privileged: true
+ },
+ crash: {
+ uri: "chrome://browser/content/aboutCrash.xhtml",
+ privileged: true
+ }
+}
+
+function AboutGeneric() {}
+
+AboutGeneric.prototype = {
+ classID: Components.ID("{433d2d75-5923-49b0-854d-f37267b03dc7}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAboutModule]),
+
+ _getModuleInfo: function (aURI) {
+ let moduleName = aURI.path.replace(/[?#].*/, "").toLowerCase();
+ return modules[moduleName];
+ },
+
+ getURIFlags: function(aURI) {
+ return Ci.nsIAboutModule.ALLOW_SCRIPT;
+ },
+
+ newChannel: function(aURI) {
+ let moduleInfo = this._getModuleInfo(aURI);
+
+ var ios = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService);
+
+ var channel = ios.newChannel(moduleInfo.uri, null, null);
+
+ if (!moduleInfo.privileged) {
+ // Setting the owner to null means that we'll go through the normal
+ // path in GetChannelPrincipal and create a codebase principal based
+ // on the channel's originalURI
+ channel.owner = null;
+ }
+
+ channel.originalURI = aURI;
+ return channel;
+ }
+};
+
+const components = [AboutGeneric];
+const NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
diff --git a/browser/metro/components/AlertsService.js b/browser/metro/components/AlertsService.js
new file mode 100644
index 000000000000..e07e38f5f6ac
--- /dev/null
+++ b/browser/metro/components/AlertsService.js
@@ -0,0 +1,27 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+// -----------------------------------------------------------------------
+// Alerts Service
+// -----------------------------------------------------------------------
+
+function AlertsService() { }
+
+AlertsService.prototype = {
+ classID: Components.ID("{fe33c107-82a4-41d6-8c64-5353267e04c9}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIAlertsService]),
+
+ showAlertNotification: function(aImageUrl, aTitle, aText, aTextClickable, aCookie, aAlertListener, aName) {
+ let browser = Services.wm.getMostRecentWindow("navigator:browser");
+ browser.AlertsHelper.showAlertNotification(aImageUrl, aTitle, aText, aTextClickable, aCookie, aAlertListener);
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([AlertsService]);
diff --git a/browser/metro/components/BrowserCLH.js b/browser/metro/components/BrowserCLH.js
new file mode 100644
index 000000000000..ff845132c6e4
--- /dev/null
+++ b/browser/metro/components/BrowserCLH.js
@@ -0,0 +1,287 @@
+/* -*- Mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const nsIBrowserSearchService = Components.interfaces.nsIBrowserSearchService;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+function openWindow(aParent, aURL, aTarget, aFeatures, aArgs) {
+ let argString = null;
+ if (aArgs && !(aArgs instanceof Ci.nsISupportsArray)) {
+ argString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
+ argString.data = aArgs;
+ }
+
+ return Services.ww.openWindow(aParent, aURL, aTarget, aFeatures, argString || aArgs);
+}
+
+function resolveURIInternal(aCmdLine, aArgument) {
+ let uri = aCmdLine.resolveURI(aArgument);
+
+ if (!(uri instanceof Ci.nsIFileURL))
+ return uri;
+
+ try {
+ if (uri.file.exists())
+ return uri;
+ } catch (e) {
+ Cu.reportError(e);
+ }
+
+ try {
+ let urifixup = Cc["@mozilla.org/docshell/urifixup;1"].getService(Ci.nsIURIFixup);
+ uri = urifixup.createFixupURI(aArgument, 0);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+
+ return uri;
+}
+
+/**
+ * Determines whether a home page override is needed.
+ * Returns:
+ * "new profile" if this is the first run with a new profile.
+ * "new version" if this is the first run with a build with a different
+ * Gecko milestone (i.e. right after an upgrade).
+ * "none" otherwise.
+ */
+function needHomepageOverride() {
+ let savedmstone = null;
+ try {
+ savedmstone = Services.prefs.getCharPref("browser.startup.homepage_override.mstone");
+ } catch (e) {}
+
+ if (savedmstone == "ignore")
+ return "none";
+
+#expand let ourmstone = "__MOZ_APP_VERSION__";
+
+ if (ourmstone != savedmstone) {
+ Services.prefs.setCharPref("browser.startup.homepage_override.mstone", ourmstone);
+
+ return (savedmstone ? "new version" : "new profile");
+ }
+
+ return "none";
+}
+
+function getHomePage() {
+ let url = "about:start";
+ try {
+ url = Services.prefs.getComplexValue("browser.startup.homepage", Ci.nsIPrefLocalizedString).data;
+ } catch (e) { }
+
+ return url;
+}
+
+function showPanelWhenReady(aWindow, aPage) {
+ aWindow.addEventListener("UIReadyDelayed", function(aEvent) {
+ aWindow.PanelUI.show(aPage);
+ }, false);
+}
+
+function haveSystemLocale() {
+ let localeService = Cc["@mozilla.org/intl/nslocaleservice;1"].getService(Ci.nsILocaleService);
+ let systemLocale = localeService.getSystemLocale().getCategory("NSILOCALE_CTYPE");
+ return isLocaleAvailable(systemLocale);
+}
+
+function checkCurrentLocale() {
+ if (Services.prefs.prefHasUserValue("general.useragent.locale")) {
+ // if the user has a compatible locale from a different buildid, we need to update
+ var buildID = Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo).platformBuildID;
+ let localeBuildID = Services.prefs.getCharPref("extensions.compatability.locales.buildid");
+ if (buildID != localeBuildID)
+ return false;
+
+ let currentLocale = Services.prefs.getCharPref("general.useragent.locale");
+ return isLocaleAvailable(currentLocale);
+ }
+ return true;
+}
+
+function isLocaleAvailable(aLocale) {
+ let chrome = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIXULChromeRegistry);
+ chrome.QueryInterface(Ci.nsIToolkitChromeRegistry);
+ let availableLocales = chrome.getLocalesForPackage("browser");
+
+ let locale = aLocale.split("-")[0];
+ let localeRegEx = new RegExp("^" + locale);
+
+ while (availableLocales.hasMore()) {
+ let locale = availableLocales.getNext();
+ if (localeRegEx.test(locale))
+ return true;
+ }
+ return false;
+}
+
+function BrowserCLH() { }
+
+BrowserCLH.prototype = {
+ //
+ // nsICommandLineHandler
+ //
+ handle: function fs_handle(aCmdLine) {
+#ifdef DEBUG
+ for (var idx = 0; idx < aCmdLine.length; idx++) {
+ dump(aCmdLine.getArgument(idx) + "\n");
+ }
+#endif
+ // Instantiate the search service so the search engine cache is created now
+ // instead when the application is running. The install process will register
+ // this component by using the -silent command line flag, thereby creating
+ // the cache during install, not runtime.
+ // NOTE: This code assumes this CLH is run before the nsDefaultCLH, which
+ // consumes the "-silent" flag.
+ if (aCmdLine.findFlag("silent", false) > -1) {
+ let searchService = Services.search;
+ let autoComplete = Cc["@mozilla.org/autocomplete/search;1?name=history"].
+ getService(Ci.nsIAutoCompleteSearch);
+ return;
+ }
+
+ // Handle chrome windows loaded via commandline
+ let chromeParam = aCmdLine.handleFlagWithParam("chrome", false);
+ if (chromeParam) {
+ try {
+ // Only load URIs which do not inherit chrome privs
+ let features = "chrome,dialog=no,all";
+ let uri = resolveURIInternal(aCmdLine, chromeParam);
+ let netutil = Cc["@mozilla.org/network/util;1"].getService(Ci.nsINetUtil);
+ if (!netutil.URIChainHasFlags(uri, Ci.nsIHttpProtocolHandler.URI_INHERITS_SECURITY_CONTEXT)) {
+ openWindow(null, uri.spec, "_blank", features, null);
+
+ // Stop the normal commandline processing from continuing
+ aCmdLine.preventDefault = true;
+ }
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ return;
+ }
+
+ // Check and remove the alert flag here, but we'll handle it a bit later - see below
+ let alertFlag = aCmdLine.handleFlagWithParam("alert", false);
+
+ // Check and remove the webapp param
+ let appFlag = aCmdLine.handleFlagWithParam("webapp", false);
+ let appURI;
+ if (appFlag)
+ appURI = resolveURIInternal(aCmdLine, appFlag);
+
+ // Keep an array of possible URL arguments
+ let uris = [];
+
+ // Check for the "url" flag
+ let uriFlag = aCmdLine.handleFlagWithParam("url", false);
+ if (uriFlag) {
+ let uri = resolveURIInternal(aCmdLine, uriFlag);
+ if (uri)
+ uris.push(uri);
+ }
+
+ // Check for the "search" flag
+ let searchParam = aCmdLine.handleFlagWithParam("search", false);
+ if (searchParam) {
+ var ss = Components.classes["@mozilla.org/browser/search-service;1"]
+ .getService(nsIBrowserSearchService);
+ var submission = ss.defaultEngine.getSubmission(searchParam.replace("\"", "", "g"));
+ uris.push(submission.uri);
+ }
+
+ for (let i = 0; i < aCmdLine.length; i++) {
+ let arg = aCmdLine.getArgument(i);
+ if (!arg || arg[0] == '-')
+ continue;
+
+ let uri = resolveURIInternal(aCmdLine, arg);
+ if (uri)
+ uris.push(uri);
+ }
+
+ // Open the main browser window, if we don't already have one
+ let browserWin;
+ try {
+ let localeWin = Services.wm.getMostRecentWindow("navigator:localepicker");
+ if (localeWin) {
+ localeWin.focus();
+ aCmdLine.preventDefault = true;
+ return;
+ }
+
+ browserWin = Services.wm.getMostRecentWindow("navigator:browser");
+ if (!browserWin) {
+ // Default to the saved homepage
+ let defaultURL = getHomePage();
+
+ // Override the default if we have a URL passed on command line
+ if (uris.length > 0) {
+ defaultURL = uris[0].spec;
+ uris = uris.slice(1);
+ }
+
+ // Show the locale selector if we have a new profile, or if the selected locale is no longer compatible
+ let showLocalePicker = Services.prefs.getBoolPref("browser.firstrun.show.localepicker");
+ if ((needHomepageOverride() == "new profile" && showLocalePicker && !haveSystemLocale())) { // || !checkCurrentLocale()) {
+ browserWin = openWindow(null, "chrome://browser/content/localePicker.xul", "_blank", "chrome,dialog=no,all", defaultURL);
+ aCmdLine.preventDefault = true;
+ return;
+ }
+
+ browserWin = openWindow(null, "chrome://browser/content/browser.xul", "_blank", "chrome,dialog=no,all", defaultURL);
+ }
+
+ browserWin.focus();
+
+ // Stop the normal commandline processing from continuing. We just opened the main browser window
+ aCmdLine.preventDefault = true;
+ } catch (e) {
+ Cu.reportError(e);
+ }
+
+ // Assumption: All remaining command line arguments have been sent remotely (browser is already running)
+ // Action: Open any URLs we find into an existing browser window
+
+ // First, get a browserDOMWindow object
+ while (!browserWin.browserDOMWindow)
+ Services.tm.currentThread.processNextEvent(true);
+
+ // Open any URIs into new tabs
+ for (let i = 0; i < uris.length; i++)
+ browserWin.browserDOMWindow.openURI(uris[i], null, Ci.nsIBrowserDOMWindow.OPEN_NEWTAB, Ci.nsIBrowserDOMWindow.OPEN_EXTERNAL);
+
+ if (appURI)
+ browserWin.browserDOMWindow.openURI(appURI, null, browserWin.OPEN_APPTAB, Ci.nsIBrowserDOMWindow.OPEN_NEW);
+
+ // Handle the notification, if called from it
+ if (alertFlag) {
+ if (alertFlag == "update-app") {
+ // Notification was already displayed and clicked, skip it next time
+ Services.prefs.setBoolPref("app.update.skipNotification", true);
+
+ var updateService = Cc["@mozilla.org/updates/update-service;1"].getService(Ci.nsIApplicationUpdateService);
+ var updateTimerCallback = updateService.QueryInterface(Ci.nsITimerCallback);
+ updateTimerCallback.notify(null);
+ } else if (alertFlag.length >= 9 && alertFlag.substr(0, 9) == "download:") {
+ showPanelWhenReady(browserWin, "downloads-container");
+ }
+ }
+ },
+
+ // QI
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICommandLineHandler]),
+
+ // XPCOMUtils factory
+ classID: Components.ID("{be623d20-d305-11de-8a39-0800200c9a66}"),
+};
+
+var components = [ BrowserCLH ];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
diff --git a/browser/metro/components/BrowserStartup.js b/browser/metro/components/BrowserStartup.js
new file mode 100644
index 000000000000..37b57cde4bc7
--- /dev/null
+++ b/browser/metro/components/BrowserStartup.js
@@ -0,0 +1,117 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+// Custom factory object to ensure that we're a singleton
+const BrowserStartupServiceFactory = {
+ _instance: null,
+ createInstance: function (outer, iid) {
+ if (outer != null)
+ throw Components.results.NS_ERROR_NO_AGGREGATION;
+ return this._instance || (this._instance = new BrowserStartup());
+ }
+};
+
+function BrowserStartup() {
+ this._init();
+}
+
+BrowserStartup.prototype = {
+ // for XPCOM
+ classID: Components.ID("{1d542abc-c88b-4636-a4ef-075b49806317}"),
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver, Ci.nsISupportsWeakReference]),
+
+ _xpcom_factory: BrowserStartupServiceFactory,
+
+ _init: function() {
+ Services.obs.addObserver(this, "places-init-complete", false);
+ Services.obs.addObserver(this, "final-ui-startup", false);
+ },
+
+ _initDefaultBookmarks: function() {
+ // We must instantiate the history service since it will tell us if we
+ // need to import or restore bookmarks due to first-run, corruption or
+ // forced migration (due to a major schema change).
+ let histsvc = Cc["@mozilla.org/browser/nav-history-service;1"].
+ getService(Ci.nsINavHistoryService);
+
+ // If the database is corrupt or has been newly created we should
+ // import bookmarks.
+ let databaseStatus = histsvc.databaseStatus;
+ let importBookmarks = databaseStatus == histsvc.DATABASE_STATUS_CREATE ||
+ databaseStatus == histsvc.DATABASE_STATUS_CORRUPT;
+
+ if (!importBookmarks) {
+ // Check to see whether "mobile" root already exists. This is to handle
+ // existing profiles created with pre-1.0 builds (which won't have mobile
+ // bookmarks root). We can remove this eventually when we stop
+ // caring about users migrating to current builds with pre-1.0 profiles.
+ let annos = Cc["@mozilla.org/browser/annotation-service;1"].
+ getService(Ci.nsIAnnotationService);
+ let metroRootItems = annos.getItemsWithAnnotation("metro/bookmarksRoot", {});
+ if (metroRootItems.length > 0)
+ return; // no need to do initial import
+ }
+
+ Cu.import("resource://gre/modules/PlacesUtils.jsm");
+
+ try {
+ let observer = {
+ onStreamComplete : function(aLoader, aContext, aStatus, aLength, aResult) {
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ let jsonStr = "";
+ try {
+ converter.charset = "UTF-8";
+ jsonStr = converter.convertFromByteArray(aResult, aResult.length);
+
+ // aReplace=false since this may be called when there are existing
+ // bookmarks that we don't want to overwrite ("no mobile root"
+ // case from above)
+ PlacesUtils.restoreBookmarksFromJSONString(jsonStr, false);
+ } catch (err) {
+ Cu.reportError("Failed to parse default bookmarks from bookmarks.json: " + err);
+ }
+ }
+ };
+
+ let ioSvc = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService);
+ let uri = ioSvc.newURI("chrome://browser/locale/bookmarks.json", null, null);
+ let channel = ioSvc.newChannelFromURI(uri);
+ let sl = Cc["@mozilla.org/network/stream-loader;1"].
+ createInstance(Ci.nsIStreamLoader);
+ sl.init(observer);
+ channel.asyncOpen(sl, channel);
+ } catch (err) {
+ // Report the error, but ignore it.
+ Cu.reportError("Failed to load default bookmarks from bookmarks.json: " + err);
+ }
+ },
+
+ _startupActions: function() {
+ },
+
+ // nsIObserver
+ observe: function(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "places-init-complete":
+ Services.obs.removeObserver(this, "places-init-complete");
+ this._initDefaultBookmarks();
+ break;
+ case "final-ui-startup":
+ Services.obs.removeObserver(this, "final-ui-startup");
+ this._startupActions();
+ break;
+ }
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([BrowserStartup]);
diff --git a/browser/metro/components/CapturePicker.js b/browser/metro/components/CapturePicker.js
new file mode 100644
index 000000000000..8458dfcf4543
--- /dev/null
+++ b/browser/metro/components/CapturePicker.js
@@ -0,0 +1,85 @@
+/* -*- Mode: js2; js2-basic-offset: 4; indent-tabs-mode: nil; -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+function CapturePicker() {
+ this.messageManager = Cc["@mozilla.org/childprocessmessagemanager;1"].getService(Ci.nsISyncMessageSender);
+}
+
+CapturePicker.prototype = {
+ _file: null,
+ _mode: -1,
+ _result: -1,
+ _shown: false,
+ _title: "",
+ _type: "",
+ _window: null,
+
+ //
+ // nsICapturePicker
+ //
+ init: function(aWindow, aTitle, aMode) {
+ this._window = aWindow;
+ this._title = aTitle;
+ this._mode = aMode;
+ },
+
+ show: function() {
+ if (this._shown)
+ throw Cr.NS_ERROR_UNEXPECTED;
+
+ this._shown = true;
+
+ let res = this.messageManager.sendSyncMessage("CapturePicker:Show", { title: this._title, mode: this._mode, type: this._type })[0];
+ if (res.value)
+ this._file = res.path;
+
+ return (res.value ? Ci.nsICapturePicker.RETURN_OK : Ci.nsICapturePicker.RETURN_CANCEL);
+ },
+
+ modeMayBeAvailable: function(aMode) {
+ if (aMode != Ci.nsICapturePicker.MODE_STILL)
+ return false;
+ return true;
+ },
+
+ get file() {
+ if (this._file) {
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+ file.initWithPath(this._file);
+ let utils = this._window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ return utils.wrapDOMFile(file);
+ } else {
+ throw Cr.NS_ERROR_FAILURE;
+ }
+ },
+
+ get type() {
+ return this._type;
+ },
+
+ set type(aNewType) {
+ if (this._shown)
+ throw Cr.NS_ERROR_UNEXPECTED;
+ else
+ this._type = aNewType;
+ },
+
+ // QI
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICapturePicker]),
+
+ // XPCOMUtils factory
+ classID: Components.ID("{cb5a47f0-b58c-4fc3-b61a-358ee95f8238}"),
+};
+
+var components = [ CapturePicker ];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
diff --git a/browser/metro/components/ContentDispatchChooser.js b/browser/metro/components/ContentDispatchChooser.js
new file mode 100644
index 000000000000..03e7e28c0a42
--- /dev/null
+++ b/browser/metro/components/ContentDispatchChooser.js
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+function ContentDispatchChooser() {}
+
+ContentDispatchChooser.prototype =
+{
+ classID: Components.ID("5a072a22-1e66-4100-afc1-07aed8b62fc5"),
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentDispatchChooser]),
+
+ ask: function ask(aHandler, aWindowContext, aURI, aReason) {
+ let window = null;
+ try {
+ if (aWindowContext)
+ window = aWindowContext.getInterface(Ci.nsIDOMWindow);
+ } catch (e) { /* it's OK to not have a window */ }
+
+ let bundle = Services.strings.createBundle("chrome://mozapps/locale/handling/handling.properties");
+
+ let title = bundle.GetStringFromName("protocol.title");
+ let message = bundle.GetStringFromName("protocol.description");
+
+ let open = Services.prompt.confirm(window, title, message);
+ if (open)
+ aHandler.launchWithURI(aURI, aWindowContext);
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ContentDispatchChooser]);
+
diff --git a/browser/metro/components/ContentPermissionPrompt.js b/browser/metro/components/ContentPermissionPrompt.js
new file mode 100644
index 000000000000..0c66d7bf493d
--- /dev/null
+++ b/browser/metro/components/ContentPermissionPrompt.js
@@ -0,0 +1,129 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const kCountBeforeWeRemember = 5;
+
+function setPagePermission(type, principal, allow) {
+ let pm = Services.perms;
+ let contentPrefs = Services.contentPrefs;
+ let contentPrefName = type + ".request.remember";
+
+ if (!contentPrefs.hasPref(principal.URI, contentPrefName))
+ contentPrefs.setPref(principal.URI, contentPrefName, 0);
+
+ let count = contentPrefs.getPref(principal.URI, contentPrefName);
+
+ if (allow == false)
+ count--;
+ else
+ count++;
+
+ contentPrefs.setPref(principal.URI, contentPrefName, count);
+ if (count == kCountBeforeWeRemember)
+ pm.addFromPrincipal(principal, type, Ci.nsIPermissionManager.ALLOW_ACTION);
+ else if (count == -kCountBeforeWeRemember)
+ pm.addFromPrincipal(principal, type, Ci.nsIPermissionManager.DENY_ACTION);
+}
+
+const kEntities = { "geolocation": "geolocation", "desktop-notification": "desktopNotification",
+ "indexedDB": "offlineApps", "indexedDBQuota": "indexedDBQuota",
+ "openWebappsManage": "openWebappsManage" };
+
+function ContentPermissionPrompt() {}
+
+ContentPermissionPrompt.prototype = {
+ classID: Components.ID("{C6E8C44D-9F39-4AF7-BCC0-76E38A8310F5}"),
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentPermissionPrompt]),
+
+ getChromeWindow: function getChromeWindow(aWindow) {
+ let chromeWin = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIWebNavigation)
+ .QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow)
+ .QueryInterface(Ci.nsIDOMChromeWindow);
+ return chromeWin;
+ },
+
+ getNotificationBoxForRequest: function getNotificationBoxForRequest(request) {
+ let notificationBox = null;
+ if (request.window) {
+ let requestingWindow = request.window.top;
+ let chromeWin = this.getChromeWindow(requestingWindow).wrappedJSObject;
+ let windowID = window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).currentInnerWindowID;
+ let browser = chromeWin.Browser.getBrowserForWindowId(windowID);
+ return chromeWin.getNotificationBox(browser);
+ }
+
+ let chromeWin = request.element.ownerDocument.defaultView;
+ return chromeWin.Browser.getNotificationBox(request.element);
+ },
+
+ handleExistingPermission: function handleExistingPermission(request) {
+ let result = Services.perms.testExactPermissionFromPrincipal(request.principal, request.type);
+ if (result == Ci.nsIPermissionManager.ALLOW_ACTION) {
+ request.allow();
+ return true;
+ }
+ if (result == Ci.nsIPermissionManager.DENY_ACTION) {
+ request.cancel();
+ return true;
+ }
+ return false;
+ },
+
+ prompt: function(request) {
+ // returns true if the request was handled
+ if (this.handleExistingPermission(request))
+ return;
+
+ let pm = Services.perms;
+ let notificationBox = this.getNotificationBoxForRequest(request);
+ let browserBundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+
+ let notification = notificationBox.getNotificationWithValue(request.type);
+ if (notification)
+ return;
+
+ let entityName = kEntities[request.type];
+
+ let buttons = [{
+ label: browserBundle.GetStringFromName(entityName + ".allow"),
+ accessKey: null,
+ callback: function(notification) {
+ setPagePermission(request.type, request.principal, true);
+ request.allow();
+ }
+ },
+ {
+ label: browserBundle.GetStringFromName(entityName + ".dontAllow"),
+ accessKey: null,
+ callback: function(notification) {
+ setPagePermission(request.type, request.principal, false);
+ request.cancel();
+ }
+ }];
+
+ let message = browserBundle.formatStringFromName(entityName + ".wantsTo",
+ [request.principal.URI.host], 1);
+ let newBar = notificationBox.appendNotification(message,
+ request.type,
+ "", // Notifications in Fennec do not display images.
+ notificationBox.PRIORITY_WARNING_MEDIUM,
+ buttons);
+ }
+};
+
+
+//module initialization
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([ContentPermissionPrompt]);
diff --git a/browser/metro/components/DirectoryProvider.js b/browser/metro/components/DirectoryProvider.js
new file mode 100644
index 000000000000..08bacdc6968a
--- /dev/null
+++ b/browser/metro/components/DirectoryProvider.js
@@ -0,0 +1,58 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+// -----------------------------------------------------------------------
+// Directory Provider for special browser folders and files
+// -----------------------------------------------------------------------
+
+const NS_APP_CACHE_PARENT_DIR = "cachePDir";
+const XRE_UPDATE_ROOT_DIR = "UpdRootD";
+const ENVVAR_UPDATE_DIR = "UPDATES_DIRECTORY";
+
+function DirectoryProvider() {}
+
+DirectoryProvider.prototype = {
+ classID: Components.ID("{ef0f7a87-c1ee-45a8-8d67-26f586e46a4b}"),
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIDirectoryServiceProvider]),
+
+ getFile: function(prop, persistent) {
+ if (prop == NS_APP_CACHE_PARENT_DIR) {
+ let dirsvc = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties);
+ let profile = dirsvc.get("ProfD", Ci.nsIFile);
+
+ let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
+ let device = sysInfo.get("device");
+ switch (device) {
+ default:
+ return profile;
+ }
+ } else if (prop == XRE_UPDATE_ROOT_DIR) {
+ let env = Cc["@mozilla.org/process/environment;1"].getService(Ci.nsIEnvironment);
+ if (env.exists(ENVVAR_UPDATE_DIR)) {
+ let path = env.get(ENVVAR_UPDATE_DIR);
+ if (path) {
+ let localFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+ localFile.initWithPath(path);
+ return localFile;
+ }
+ }
+ let dm = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
+ return dm.defaultDownloadsDirectory;
+ }
+
+ // We are retuning null to show failure instead for throwing an error. The
+ // interface is called quite a bit and throwing an error is noisy. Returning
+ // null works with the way the interface is called [see bug 529077]
+ return null;
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DirectoryProvider]);
+
diff --git a/browser/metro/components/DownloadManagerUI.js b/browser/metro/components/DownloadManagerUI.js
new file mode 100644
index 000000000000..19023d9e19ac
--- /dev/null
+++ b/browser/metro/components/DownloadManagerUI.js
@@ -0,0 +1,46 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Ci = Components.interfaces;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+// -----------------------------------------------------------------------
+// Download Manager UI
+// -----------------------------------------------------------------------
+
+function DownloadManagerUI() { }
+
+DownloadManagerUI.prototype = {
+ classID: Components.ID("{93db15b1-b408-453e-9a2b-6619e168324a}"),
+
+ show: function show(aWindowContext, aID, aReason, aUsePrivateUI) {
+ if (!aReason)
+ aReason = Ci.nsIDownloadManagerUI.REASON_USER_INTERACTED;
+
+ let browser = Services.wm.getMostRecentWindow("navigator:browser");
+ if (browser)
+ browser.showDownloadManager(aWindowContext, aID, aReason);
+ },
+
+ get visible() {
+ let browser = Services.wm.getMostRecentWindow("navigator:browser");
+ if (browser) {
+ return browser.DownloadsView.visible;
+ }
+ return false;
+ },
+
+ getAttention: function getAttention() {
+ if (this.visible)
+ this.show(null, null, null);
+ else
+ throw Cr.NS_ERROR_UNEXPECTED;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIDownloadManagerUI])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([DownloadManagerUI]);
diff --git a/browser/metro/components/FormAutoComplete.js b/browser/metro/components/FormAutoComplete.js
new file mode 100644
index 000000000000..1f2df9021983
--- /dev/null
+++ b/browser/metro/components/FormAutoComplete.js
@@ -0,0 +1,95 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+function LOG() {
+ return; // comment out for verbose debugging
+ let msg = Array.join(arguments, " ");
+ dump(msg + "\n");
+ Cu.reportError(msg);
+}
+
+// Lazily get the base Form AutoComplete Search
+XPCOMUtils.defineLazyGetter(this, "FAC", function() {
+ return Components.classesByID["{c11c21b2-71c9-4f87-a0f8-5e13f50495fd}"]
+ .getService(Ci.nsIFormAutoComplete);
+});
+
+function FormAutoComplete() {
+ LOG("new FAC");
+}
+
+FormAutoComplete.prototype = {
+ classDescription: "Form AutoComplete Plus",
+ classID: Components.ID("{cccd414c-3ec2-4cc5-9dc4-36c87cc3c4fe}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIFormAutoComplete]),
+
+ // Specify the html5 types that we want and some values to guess
+ contactTypes: {
+ email: /^(?:.*(?:e-?mail|recipients?).*|(send_)?to(_b?cc)?)$/i,
+ tel: /^(?:tel(?:ephone)?|.*phone.*)$/i
+ },
+
+ checkQueryType: function checkQueryType(aName, aField) {
+ // If we have an input field with the desired html5 type, take it!
+ if (aField && "type" in aField) {
+ let type = aField.type;
+ if (type && type in this.contactTypes)
+ return type;
+ }
+
+ // Grab properties to check for contact inputs
+ let props = [aName];
+ if (aField) {
+ let specialProps = [aField["className"], aField["id"]];
+ props = props.concat(specialProps.filter(function(aValue) {
+ return aValue;
+ }));
+ }
+
+ // Check the gathered properties for contact-like values
+ for (let [type, regex] in Iterator(this.contactTypes)) {
+ if (props.some(function(prop) prop.search(regex) != -1))
+ return type;
+ }
+ return null;
+ },
+
+ autoCompleteSearch: function autoCompleteSearch(aName, aQuery, aField, aPrev) {
+ if (!Services.prefs.getBoolPref("browser.formfill.enable"))
+ return null;
+
+ LOG("autocomplete search", Array.slice(arguments));
+ let result = Cc["@mozilla.org/autocomplete/simple-result;1"].createInstance(Ci.nsIAutoCompleteSimpleResult);
+ result.setSearchString(aQuery);
+
+ // Don't allow duplicates get merged into the final results
+ let dupCheck = {};
+
+ // Use the base form autocomplete for non-contact searches
+ let normal = FAC.autoCompleteSearch(aName, aQuery, aField, aPrev);
+ if (normal.matchCount > 0) {
+ for (let i = 0; i < normal.matchCount; i++) {
+ dupCheck[normal.getValueAt(i)] = true;
+ result.appendMatch(normal.getValueAt(i), normal.getCommentAt(i), normal.getImageAt(i), normal.getStyleAt(i));
+ }
+ }
+
+ // Do searches for certain input fields
+ let type = this.checkQueryType(aName, aField);
+
+ let resultCode = result.matchCount ? "RESULT_SUCCESS" : "RESULT_NOMATCH";
+ result.setSearchResult(Ci.nsIAutoCompleteResult[resultCode]);
+ return result;
+ }
+};
+
+let components = [FormAutoComplete];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
diff --git a/browser/metro/components/HelperAppDialog.js b/browser/metro/components/HelperAppDialog.js
new file mode 100644
index 000000000000..92091a985dd3
--- /dev/null
+++ b/browser/metro/components/HelperAppDialog.js
@@ -0,0 +1,216 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+const PREF_BD_USEDOWNLOADDIR = "browser.download.useDownloadDir";
+const URI_GENERIC_ICON_DOWNLOAD = "chrome://browser/skin/images/alert-downloads-30.png";
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+// -----------------------------------------------------------------------
+// HelperApp Launcher Dialog
+// -----------------------------------------------------------------------
+
+function HelperAppLauncherDialog() { }
+
+HelperAppLauncherDialog.prototype = {
+ classID: Components.ID("{e9d277a0-268a-4ec2-bb8c-10fdf3e44611}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIHelperAppLauncherDialog]),
+
+ show: function hald_show(aLauncher, aContext, aReason) {
+ // Check to see if we can open this file or not
+ if (aLauncher.MIMEInfo.hasDefaultHandler) {
+ aLauncher.MIMEInfo.preferredAction = Ci.nsIMIMEInfo.useSystemDefault;
+ aLauncher.launchWithApplication(null, false);
+ } else {
+ let wasClicked = false;
+ let listener = {
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == "alertclickcallback") {
+ wasClicked = true;
+ let win = Cc["@mozilla.org/appshell/window-mediator;1"].getService(Ci.nsIWindowMediator).getMostRecentWindow("navigator:browser");
+ if (win)
+ win.PanelUI.show("downloads-container");
+
+ aLauncher.saveToDisk(null, false);
+ } else {
+ if (!wasClicked)
+ aLauncher.cancel(Cr.NS_BINDING_ABORTED);
+ }
+ }
+ };
+ this._notify(aLauncher, listener);
+ }
+ },
+
+ promptForSaveToFile: function hald_promptForSaveToFile(aLauncher, aContext, aDefaultFile, aSuggestedFileExt, aForcePrompt) {
+ let file = null;
+ let prefs = Services.prefs;
+
+ if (!aForcePrompt) {
+ // Check to see if the user wishes to auto save to the default download
+ // folder without prompting. Note that preference might not be set.
+ let autodownload = true;
+ try {
+ autodownload = prefs.getBoolPref(PREF_BD_USEDOWNLOADDIR);
+ } catch (e) { }
+
+ if (autodownload) {
+ // Retrieve the user's default download directory
+ let dnldMgr = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
+ let defaultFolder = dnldMgr.userDownloadsDirectory;
+
+ try {
+ file = this.validateLeafName(defaultFolder, aDefaultFile, aSuggestedFileExt);
+ }
+ catch (e) {
+ }
+
+ // Check to make sure we have a valid directory, otherwise, prompt
+ if (file)
+ return file;
+ }
+ }
+
+ // Use file picker to show dialog.
+ let picker = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ let windowTitle = "";
+ let parent = aContext.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
+ picker.init(parent, windowTitle, Ci.nsIFilePicker.modeSave);
+ picker.defaultString = aDefaultFile;
+
+ if (aSuggestedFileExt) {
+ // aSuggestedFileExtension includes the period, so strip it
+ picker.defaultExtension = aSuggestedFileExt.substring(1);
+ }
+ else {
+ try {
+ picker.defaultExtension = aLauncher.MIMEInfo.primaryExtension;
+ }
+ catch (e) { }
+ }
+
+ var wildCardExtension = "*";
+ if (aSuggestedFileExt) {
+ wildCardExtension += aSuggestedFileExt;
+ picker.appendFilter(aLauncher.MIMEInfo.description, wildCardExtension);
+ }
+
+ picker.appendFilters(Ci.nsIFilePicker.filterAll);
+
+ // Default to lastDir if it is valid, otherwise use the user's default
+ // downloads directory. userDownloadsDirectory should always return a
+ // valid directory, so we can safely default to it.
+ var dnldMgr = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
+ picker.displayDirectory = dnldMgr.userDownloadsDirectory;
+
+ // The last directory preference may not exist, which will throw.
+ try {
+ let lastDir = prefs.getComplexValue("browser.download.lastDir", Ci.nsILocalFile);
+ if (isUsableDirectory(lastDir))
+ picker.displayDirectory = lastDir;
+ }
+ catch (e) { }
+
+ if (picker.show() == Ci.nsIFilePicker.returnCancel) {
+ // null result means user cancelled.
+ return null;
+ }
+
+ // Be sure to save the directory the user chose through the Save As...
+ // dialog as the new browser.download.dir since the old one
+ // didn't exist.
+ file = picker.file;
+
+ if (file) {
+ try {
+ // Remove the file so that it's not there when we ensure non-existence later;
+ // this is safe because for the file to exist, the user would have had to
+ // confirm that he wanted the file overwritten.
+ if (file.exists())
+ file.remove(false);
+ }
+ catch (e) { }
+ var newDir = file.parent.QueryInterface(Ci.nsILocalFile);
+ prefs.setComplexValue("browser.download.lastDir", Ci.nsILocalFile, newDir);
+ file = this.validateLeafName(newDir, file.leafName, null);
+ }
+ return file;
+ },
+
+ validateLeafName: function hald_validateLeafName(aLocalFile, aLeafName, aFileExt) {
+ if (!(aLocalFile && this.isUsableDirectory(aLocalFile)))
+ return null;
+
+ // Remove any leading periods, since we don't want to save hidden files
+ // automatically.
+ aLeafName = aLeafName.replace(/^\.+/, "");
+
+ if (aLeafName == "")
+ aLeafName = "unnamed" + (aFileExt ? "." + aFileExt : "");
+ aLocalFile.append(aLeafName);
+
+ this.makeFileUnique(aLocalFile);
+ return aLocalFile;
+ },
+
+ makeFileUnique: function hald_makeFileUnique(aLocalFile) {
+ try {
+ // Note - this code is identical to that in
+ // toolkit/content/contentAreaUtils.js.
+ // If you are updating this code, update that code too! We can't share code
+ // here since this is called in a js component.
+ var collisionCount = 0;
+ while (aLocalFile.exists()) {
+ collisionCount++;
+ if (collisionCount == 1) {
+ // Append "(2)" before the last dot in (or at the end of) the filename
+ // special case .ext.gz etc files so we don't wind up with .tar(2).gz
+ if (aLocalFile.leafName.match(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i))
+ aLocalFile.leafName = aLocalFile.leafName.replace(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i, "(2)$&");
+ else
+ aLocalFile.leafName = aLocalFile.leafName.replace(/(\.[^\.]*)?$/, "(2)$&");
+ }
+ else {
+ // replace the last (n) in the filename with (n+1)
+ aLocalFile.leafName = aLocalFile.leafName.replace(/^(.*\()\d+\)/, "$1" + (collisionCount+1) + ")");
+ }
+ }
+ aLocalFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, 0600);
+ }
+ catch (e) {
+ dump("*** exception in validateLeafName: " + e + "\n");
+
+ if (e.result == Cr.NS_ERROR_FILE_ACCESS_DENIED)
+ throw e;
+
+ if (aLocalFile.leafName == "" || aLocalFile.isDirectory()) {
+ aLocalFile.append("unnamed");
+ if (aLocalFile.exists())
+ aLocalFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0600);
+ }
+ }
+ },
+
+ isUsableDirectory: function hald_isUsableDirectory(aDirectory) {
+ return aDirectory.exists() && aDirectory.isDirectory() && aDirectory.isWritable();
+ },
+
+ _notify: function hald_notify(aLauncher, aCallback) {
+ let bundle = Services.strings.createBundle("chrome://browser/locale/browser.properties");
+
+ let notifier = Cc[aCallback ? "@mozilla.org/alerts-service;1" : "@mozilla.org/toaster-alerts-service;1"].getService(Ci.nsIAlertsService);
+ notifier.showAlertNotification(URI_GENERIC_ICON_DOWNLOAD,
+ bundle.GetStringFromName("alertDownloads"),
+ bundle.GetStringFromName("alertCantOpenDownload"),
+ true, "", aCallback, "downloadopen-fail");
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([HelperAppLauncherDialog]);
diff --git a/browser/metro/components/LoginManager.js b/browser/metro/components/LoginManager.js
new file mode 100644
index 000000000000..d9ed14d80205
--- /dev/null
+++ b/browser/metro/components/LoginManager.js
@@ -0,0 +1,686 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+function LoginManager() {
+ this.init();
+}
+
+LoginManager.prototype = {
+
+ classID: Components.ID("{f9a0edde-2a8d-4bfd-a08c-3f9333213a85}"),
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManager,
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference]),
+
+
+ /* ---------- private members ---------- */
+
+
+ __storage : null, // Storage component which contains the saved logins
+ get _storage() {
+ if (!this.__storage) {
+
+ var contractID = "@mozilla.org/login-manager/storage/mozStorage;1";
+ try {
+ var catMan = Cc["@mozilla.org/categorymanager;1"].
+ getService(Ci.nsICategoryManager);
+ contractID = catMan.getCategoryEntry("login-manager-storage",
+ "nsILoginManagerStorage");
+ this.log("Found alternate nsILoginManagerStorage with " +
+ "contract ID: " + contractID);
+ } catch (e) {
+ this.log("No alternate nsILoginManagerStorage registered");
+ }
+
+ this.__storage = Cc[contractID].
+ createInstance(Ci.nsILoginManagerStorage);
+ try {
+ this.__storage.init();
+ } catch (e) {
+ this.log("Initialization of storage component failed: " + e);
+ this.__storage = null;
+ }
+ }
+
+ return this.__storage;
+ },
+
+
+ _nsLoginInfo : null, // Constructor for nsILoginInfo implementation
+ _debug : false, // mirrors signon.debug
+ _remember : true, // mirrors signon.rememberSignons preference
+
+
+ /*
+ * init
+ *
+ * Initialize the Login Manager. Automatically called when service
+ * is created.
+ *
+ * Note: Service created in /browser/base/content/browser.js,
+ * delayedStartup()
+ */
+ init : function () {
+ // Add content listener.
+ var messageManager = Cc["@mozilla.org/globalmessagemanager;1"].
+ getService(Ci.nsIMessageListenerManager);
+ messageManager.loadFrameScript("chrome://browser/content/LoginManagerChild.js", true);
+ messageManager.addMessageListener("PasswordMgr:FormSubmitted", this);
+ messageManager.addMessageListener("PasswordMgr:GetPasswords", this);
+
+ // Get constructor for nsILoginInfo
+ this._nsLoginInfo = new Components.Constructor(
+ "@mozilla.org/login-manager/loginInfo;1", Ci.nsILoginInfo);
+
+ // Preferences. Add observer so we get notified of changes.
+ Services.prefs.addObserver("signon.", this, false);
+
+ // Get current preference values.
+ this._debug = Services.prefs.getBoolPref("signon.debug");
+ this._remember = Services.prefs.getBoolPref("signon.rememberSignons");
+
+ // Listen for shutdown to clean up
+ Services.obs.addObserver(this, "xpcom-shutdown", false);
+ },
+
+ /*
+ * log
+ *
+ * Internal function for logging debug messages to the Error Console window
+ */
+ log : function (message) {
+ if (!this._debug)
+ return;
+ dump("PasswordUtils: " + message + "\n");
+ Services.console.logStringMessage("PasswordUtils: " + message);
+ },
+
+ /*
+ * observe
+ *
+ * Implements nsIObserver for preferences and shutdown.
+ */
+ observe : function (subject, topic, data) {
+ if (topic == "nsPref:changed") {
+ this._pwmgr._debug = Services.prefs.getBoolPref("signon.debug");
+ this._pwmgr._remember = Services.prefs.getBoolPref("signon.rememberSignons");
+ } else if (topic == "xpcom-shutdown") {
+ // Circular reference forms when we mark an input field as managed
+ // by the password manager
+ this._formFillService = null;
+ } else {
+ this._pwmgr.log("Oops! Unexpected notification: " + topic);
+ }
+ },
+
+ /*
+ * receiveMessage
+ *
+ * Receives messages from content process.
+ */
+ receiveMessage: function (message) {
+ // local helper function
+ function getPrompter(aBrowser) {
+ var prompterSvc = Cc["@mozilla.org/login-manager/prompter;1"].
+ createInstance(Ci.nsILoginManagerPrompter);
+ prompterSvc.init(aBrowser);
+ return prompterSvc;
+ }
+
+ switch (message.name) {
+ case "PasswordMgr:GetPasswords":
+ // If there are no logins for this site, bail out now.
+ if (!this.countLogins(message.json.formOrigin, "", null))
+ return { foundLogins: {} };
+
+ var foundLogins = {};
+
+ if (!this.uiBusy) {
+ for (var i = 0; i < message.json.actionOrigins.length; i++) {
+ var actionOrigin = message.json.actionOrigins[i];
+ var logins = this.findLogins({}, message.json.formOrigin, actionOrigin, null);
+ if (logins.length) {
+ foundLogins[actionOrigin] = logins;
+ }
+ }
+ }
+
+ return {
+ uiBusy: this.uiBusy,
+ foundLogins: foundLogins
+ };
+
+ case "PasswordMgr:FormSubmitted":
+ var json = message.json;
+ var hostname = json.hostname;
+ var formSubmitURL = json.formSubmitURL;
+
+ if (!this.getLoginSavingEnabled(hostname)) {
+ this.log("(form submission ignored -- saving is " +
+ "disabled for: " + hostname + ")");
+ return {};
+ }
+
+ var browser = message.target;
+
+ var formLogin = new this._nsLoginInfo();
+
+ formLogin.init(hostname, formSubmitURL, null,
+ json.usernameValue,
+ json.passwordValue,
+ json.usernameField,
+ json.passwordField);
+
+ // If we didn't find a username field, but seem to be changing a
+ // password, allow the user to select from a list of applicable
+ // logins to update the password for.
+ if (!json.usernameField && json.hasOldPasswordField) {
+
+ var logins = this.findLogins({}, hostname, formSubmitURL, null);
+
+ if (logins.length == 0) {
+ // Could prompt to save this as a new password-only login.
+ // This seems uncommon, and might be wrong, so ignore.
+ this.log("(no logins for this host -- pwchange ignored)");
+ return {};
+ }
+
+ var prompter = getPrompter(browser);
+
+ if (logins.length == 1) {
+ var oldLogin = logins[0];
+ formLogin.username = oldLogin.username;
+ formLogin.usernameField = oldLogin.usernameField;
+
+ prompter.promptToChangePassword(oldLogin, formLogin);
+ } else {
+ prompter.promptToChangePasswordWithUsernames(
+ logins, logins.length, formLogin);
+ }
+
+ } else {
+
+ // Look for an existing login that matches the form login.
+ var existingLogin = null;
+ var logins = this.findLogins({}, hostname, formSubmitURL, null);
+
+ for (var i = 0; i < logins.length; i++) {
+ var same, login = logins[i];
+
+ // If one login has a username but the other doesn't, ignore
+ // the username when comparing and only match if they have the
+ // same password. Otherwise, compare the logins and match even
+ // if the passwords differ.
+ if (!login.username && formLogin.username) {
+ var restoreMe = formLogin.username;
+ formLogin.username = "";
+ same = formLogin.matches(login, false);
+ formLogin.username = restoreMe;
+ } else if (!formLogin.username && login.username) {
+ formLogin.username = login.username;
+ same = formLogin.matches(login, false);
+ formLogin.username = ""; // we know it's always blank.
+ } else {
+ same = formLogin.matches(login, true);
+ }
+
+ if (same) {
+ existingLogin = login;
+ break;
+ }
+ }
+
+ if (existingLogin) {
+ this.log("Found an existing login matching this form submission");
+
+ // Change password if needed.
+ if (existingLogin.password != formLogin.password) {
+ this.log("...passwords differ, prompting to change.");
+ prompter = getPrompter(browser);
+ prompter.promptToChangePassword(existingLogin, formLogin);
+ } else {
+ // Update the lastUsed timestamp.
+ var propBag = Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(Ci.nsIWritablePropertyBag);
+ propBag.setProperty("timeLastUsed", Date.now());
+ propBag.setProperty("timesUsedIncrement", 1);
+ this.modifyLogin(existingLogin, propBag);
+ }
+
+ return {};
+ }
+
+
+ // Prompt user to save login (via dialog or notification bar)
+ prompter = getPrompter(browser);
+ prompter.promptToSavePassword(formLogin);
+ }
+ return {};
+
+ default:
+ throw "Unexpected message " + message.name;
+ }
+ },
+
+
+ /*
+ * _getPasswordOrigin
+ *
+ * Get the parts of the URL we want for identification.
+ */
+ _getPasswordOrigin : function (uriString, allowJS) {
+ var realm = "";
+ try {
+ var uri = Services.io.newURI(uriString, null, null);
+
+ if (allowJS && uri.scheme == "javascript")
+ return "javascript:"
+
+ realm = uri.scheme + "://" + uri.host;
+
+ // If the URI explicitly specified a port, only include it when
+ // it's not the default. (We never want "http://foo.com:80")
+ var port = uri.port;
+ if (port != -1) {
+ var handler = Services.io.getProtocolHandler(uri.scheme);
+ if (port != handler.defaultPort)
+ realm += ":" + port;
+ }
+
+ } catch (e) {
+ // bug 159484 - disallow url types that don't support a hostPort.
+ // (although we handle "javascript:..." as a special case above.)
+ this.log("Couldn't parse origin for " + uriString);
+ realm = null;
+ }
+
+ return realm;
+ },
+
+
+ _getActionOrigin : function (form) {
+ var uriString = form.action;
+
+ // A blank or mission action submits to where it came from.
+ if (uriString == "")
+ uriString = form.baseURI; // ala bug 297761
+
+ return this._getPasswordOrigin(uriString, true);
+ },
+
+
+ /* ---------- Primary Public interfaces ---------- */
+
+
+ /*
+ * fillForm
+ *
+ * Fill the form with login information if we can find it.
+ */
+ fillForm : function (form) {
+ // XXX figure out what to do about fillForm
+ return false;
+ },
+
+
+ /*
+ * addLogin
+ *
+ * Add a new login to login storage.
+ */
+ addLogin : function (login) {
+ // Sanity check the login
+ if (login.hostname == null || login.hostname.length == 0)
+ throw "Can't add a login with a null or empty hostname.";
+
+ // For logins w/o a username, set to "", not null.
+ if (login.username == null)
+ throw "Can't add a login with a null username.";
+
+ if (login.password == null || login.password.length == 0)
+ throw "Can't add a login with a null or empty password.";
+
+ if (login.formSubmitURL || login.formSubmitURL == "") {
+ // We have a form submit URL. Can't have a HTTP realm.
+ if (login.httpRealm != null)
+ throw "Can't add a login with both a httpRealm and formSubmitURL.";
+ } else if (login.httpRealm) {
+ // We have a HTTP realm. Can't have a form submit URL.
+ if (login.formSubmitURL != null)
+ throw "Can't add a login with both a httpRealm and formSubmitURL.";
+ } else {
+ // Need one or the other!
+ throw "Can't add a login without a httpRealm or formSubmitURL.";
+ }
+
+
+ // Look for an existing entry.
+ var logins = this.findLogins({}, login.hostname, login.formSubmitURL,
+ login.httpRealm);
+
+ if (logins.some(function(l) login.matches(l, true)))
+ throw "This login already exists.";
+
+ this.log("Adding login: " + login);
+ return this._storage.addLogin(login);
+ },
+
+
+ /*
+ * removeLogin
+ *
+ * Remove the specified login from the stored logins.
+ */
+ removeLogin : function (login) {
+ this.log("Removing login: " + login);
+ return this._storage.removeLogin(login);
+ },
+
+
+ /*
+ * modifyLogin
+ *
+ * Change the specified login to match the new login.
+ */
+ modifyLogin : function (oldLogin, newLogin) {
+ this.log("Modifying oldLogin: " + oldLogin + " newLogin: " + newLogin);
+ return this._storage.modifyLogin(oldLogin, newLogin);
+ },
+
+
+ /*
+ * getAllLogins
+ *
+ * Get a dump of all stored logins. Used by the login manager UI.
+ *
+ * |count| is only needed for XPCOM.
+ *
+ * Returns an array of logins. If there are no logins, the array is empty.
+ */
+ getAllLogins : function (count) {
+ this.log("Getting a list of all logins");
+ return this._storage.getAllLogins(count);
+ },
+
+
+ /*
+ * removeAllLogins
+ *
+ * Remove all stored logins.
+ */
+ removeAllLogins : function () {
+ this.log("Removing all logins");
+ this._storage.removeAllLogins();
+ },
+
+ /*
+ * getAllDisabledHosts
+ *
+ * Get a list of all hosts for which logins are disabled.
+ *
+ * |count| is only needed for XPCOM.
+ *
+ * Returns an array of disabled logins. If there are no disabled logins,
+ * the array is empty.
+ */
+ getAllDisabledHosts : function (count) {
+ this.log("Getting a list of all disabled hosts");
+ return this._storage.getAllDisabledHosts(count);
+ },
+
+
+ /*
+ * findLogins
+ *
+ * Search for the known logins for entries matching the specified criteria.
+ */
+ findLogins : function (count, hostname, formSubmitURL, httpRealm) {
+ this.log("Searching for logins matching host: " + hostname +
+ ", formSubmitURL: " + formSubmitURL + ", httpRealm: " + httpRealm);
+
+ return this._storage.findLogins(count, hostname, formSubmitURL,
+ httpRealm);
+ },
+
+
+ /*
+ * searchLogins
+ *
+ * Public wrapper around _searchLogins to convert the nsIPropertyBag to a
+ * JavaScript object and decrypt the results.
+ *
+ * Returns an array of decrypted nsILoginInfo.
+ */
+ searchLogins : function(count, matchData) {
+ this.log("Searching for logins");
+
+ return this._storage.searchLogins(count, matchData);
+ },
+
+
+ /*
+ * countLogins
+ *
+ * Search for the known logins for entries matching the specified criteria,
+ * returns only the count.
+ */
+ countLogins : function (hostname, formSubmitURL, httpRealm) {
+ this.log("Counting logins matching host: " + hostname +
+ ", formSubmitURL: " + formSubmitURL + ", httpRealm: " + httpRealm);
+
+ return this._storage.countLogins(hostname, formSubmitURL, httpRealm);
+ },
+
+
+ /*
+ * uiBusy
+ */
+ get uiBusy() {
+ return this._storage.uiBusy;
+ },
+
+
+ /*
+ * getLoginSavingEnabled
+ *
+ * Check to see if user has disabled saving logins for the host.
+ */
+ getLoginSavingEnabled : function (host) {
+ this.log("Checking if logins to " + host + " can be saved.");
+ if (!this._remember)
+ return false;
+
+ return this._storage.getLoginSavingEnabled(host);
+ },
+
+
+ /*
+ * setLoginSavingEnabled
+ *
+ * Enable or disable storing logins for the specified host.
+ */
+ setLoginSavingEnabled : function (hostname, enabled) {
+ // Nulls won't round-trip with getAllDisabledHosts().
+ if (hostname.indexOf("\0") != -1)
+ throw "Invalid hostname";
+
+ this.log("Saving logins for " + hostname + " enabled? " + enabled);
+ return this._storage.setLoginSavingEnabled(hostname, enabled);
+ },
+
+
+ /*
+ * autoCompleteSearch
+ *
+ * Yuck. This is called directly by satchel:
+ * nsFormFillController::StartSearch()
+ * [toolkit/components/satchel/src/nsFormFillController.cpp]
+ *
+ * We really ought to have a simple way for code to register an
+ * auto-complete provider, and not have satchel calling pwmgr directly.
+ */
+ autoCompleteSearch : function (aSearchString, aPreviousResult, aElement) {
+ // aPreviousResult & aResult are nsIAutoCompleteResult,
+ // aElement is nsIDOMHTMLInputElement
+
+ if (!this._remember)
+ return null;
+
+ this.log("AutoCompleteSearch invoked. Search is: " + aSearchString);
+
+ var result = null;
+
+ if (aPreviousResult &&
+ aSearchString.substr(0, aPreviousResult.searchString.length) == aPreviousResult.searchString) {
+ this.log("Using previous autocomplete result");
+ result = aPreviousResult;
+ result.wrappedJSObject.searchString = aSearchString;
+
+ // We have a list of results for a shorter search string, so just
+ // filter them further based on the new search string.
+ // Count backwards, because result.matchCount is decremented
+ // when we remove an entry.
+ for (var i = result.matchCount - 1; i >= 0; i--) {
+ var match = result.getValueAt(i);
+
+ // Remove results that are too short, or have different prefix.
+ if (aSearchString.length > match.length ||
+ aSearchString.toLowerCase() !=
+ match.substr(0, aSearchString.length).toLowerCase())
+ {
+ this.log("Removing autocomplete entry '" + match + "'");
+ result.removeValueAt(i, false);
+ }
+ }
+ } else {
+ this.log("Creating new autocomplete search result.");
+
+ var doc = aElement.ownerDocument;
+ var origin = this._getPasswordOrigin(doc.documentURI);
+ var actionOrigin = this._getActionOrigin(aElement.form);
+
+ // This shouldn't trigger a master password prompt, because we
+ // don't attach to the input until after we successfully obtain
+ // logins for the form.
+ var logins = this.findLogins({}, origin, actionOrigin, null);
+ var matchingLogins = [];
+
+ // Filter out logins that don't match the search prefix. Also
+ // filter logins without a username, since that's confusing to see
+ // in the dropdown and we can't autocomplete them anyway.
+ for (i = 0; i < logins.length; i++) {
+ var username = logins[i].username.toLowerCase();
+ if (username &&
+ aSearchString.length <= username.length &&
+ aSearchString.toLowerCase() ==
+ username.substr(0, aSearchString.length))
+ {
+ matchingLogins.push(logins[i]);
+ }
+ }
+ this.log(matchingLogins.length + " autocomplete logins avail.");
+ result = new UserAutoCompleteResult(aSearchString, matchingLogins);
+ }
+
+ return result;
+ }
+}; // end of LoginManager implementation
+
+
+
+
+// nsIAutoCompleteResult implementation
+function UserAutoCompleteResult (aSearchString, matchingLogins) {
+ function loginSort(a,b) {
+ var userA = a.username.toLowerCase();
+ var userB = b.username.toLowerCase();
+
+ if (userA < userB)
+ return -1;
+
+ if (userB > userA)
+ return 1;
+
+ return 0;
+ };
+
+ this.searchString = aSearchString;
+ this.logins = matchingLogins.sort(loginSort);
+ this.matchCount = matchingLogins.length;
+
+ if (this.matchCount > 0) {
+ this.searchResult = Ci.nsIAutoCompleteResult.RESULT_SUCCESS;
+ this.defaultIndex = 0;
+ }
+}
+
+UserAutoCompleteResult.prototype = {
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsIAutoCompleteResult,
+ Ci.nsISupportsWeakReference]),
+
+ // private
+ logins : null,
+
+ // Allow autoCompleteSearch to get at the JS object so it can
+ // modify some readonly properties for internal use.
+ get wrappedJSObject() {
+ return this;
+ },
+
+ // Interfaces from idl...
+ searchString : null,
+ searchResult : Ci.nsIAutoCompleteResult.RESULT_NOMATCH,
+ defaultIndex : -1,
+ errorDescription : "",
+ matchCount : 0,
+
+ getValueAt : function (index) {
+ if (index < 0 || index >= this.logins.length)
+ throw "Index out of range.";
+
+ return this.logins[index].username;
+ },
+
+ getLabelAt : function (index) {
+ return this.getValueAt(index);
+ },
+
+ getCommentAt : function (index) {
+ return "";
+ },
+
+ getStyleAt : function (index) {
+ return "";
+ },
+
+ getImageAt : function (index) {
+ return "";
+ },
+
+ removeValueAt : function (index, removeFromDB) {
+ if (index < 0 || index >= this.logins.length)
+ throw "Index out of range.";
+
+ var [removedLogin] = this.logins.splice(index, 1);
+
+ this.matchCount--;
+ if (this.defaultIndex > this.logins.length)
+ this.defaultIndex--;
+
+ if (removeFromDB) {
+ var pwmgr = Cc["@mozilla.org/login-manager;1"].
+ getService(Ci.nsILoginManager);
+ pwmgr.removeLogin(removedLogin);
+ }
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([LoginManager]);
diff --git a/browser/metro/components/LoginManagerPrompter.idl b/browser/metro/components/LoginManagerPrompter.idl
new file mode 100644
index 000000000000..fe1961842360
--- /dev/null
+++ b/browser/metro/components/LoginManagerPrompter.idl
@@ -0,0 +1,67 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+#include "nsISupports.idl"
+
+interface nsILoginInfo;
+interface nsIFrameLoaderOwner;
+
+[scriptable, uuid(3f385080-aef5-11df-94e2-0800200c9a66)]
+
+interface nsILoginManagerPrompter : nsISupports {
+ /**
+ * Initialize the prompter. Must be called before using other interfaces.
+ *
+ * @param aBrowser
+ * A browser element in which the user is doing some
+ * login-related action in need to prompt them for something.
+ * The prompt will be associated with browser.
+ */
+ void init(in nsIFrameLoaderOwner aBrowser);
+
+ /**
+ * Ask the user if they want to save a login (Yes, Never, Not Now)
+ *
+ * @param aLogin
+ * The login to be saved.
+ */
+ void promptToSavePassword(in nsILoginInfo aLogin);
+
+ /**
+ * Ask the user if they want to change a login's password. If the
+ * user consents, modifyLogin() will be called.
+ *
+ * @param aOldLogin
+ * The existing login (with the old password).
+ * @param aNewLogin
+ * The new login.
+ */
+ void promptToChangePassword(in nsILoginInfo aOldLogin,
+ in nsILoginInfo aNewLogin);
+
+ /**
+ * Ask the user if they want to change the password for one of
+ * multiple logins, when the caller can't determine exactly which
+ * login should be changed. If the user consents, modifyLogin() will
+ * be called.
+ *
+ * @param logins
+ * An array of existing logins.
+ * @param count
+ * (length of the array)
+ * @param aNewLogin
+ * The new login.
+ *
+ * Note: Because the caller does not know the username of the login
+ * to be changed, aNewLogin.username and aNewLogin.usernameField
+ * will be set (using the user's selection) before modifyLogin()
+ * is called.
+ */
+ void promptToChangePasswordWithUsernames(
+ [array, size_is(count)] in nsILoginInfo logins,
+ in uint32_t count,
+ in nsILoginInfo aNewLogin);
+};
+
diff --git a/browser/metro/components/LoginManagerPrompter.js b/browser/metro/components/LoginManagerPrompter.js
new file mode 100644
index 000000000000..35cebc46c806
--- /dev/null
+++ b/browser/metro/components/LoginManagerPrompter.js
@@ -0,0 +1,610 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+/* ==================== LoginManagerPrompter ==================== */
+/*
+ * LoginManagerPrompter
+ *
+ * Implements interfaces for prompting the user to enter/save/change auth info.
+ *
+ * nsILoginManagerPrompter: Used by Login Manager for saving/changing logins
+ * found in HTML forms.
+ */
+function LoginManagerPrompter() {
+}
+
+LoginManagerPrompter.prototype = {
+
+ classID : Components.ID("97d12931-abe2-11df-94e2-0800200c9a66"),
+ QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManagerPrompter]),
+
+ _factory : null,
+ _browser : null,
+ _debug : false, // mirrors signon.debug
+
+ __pwmgr : null, // Password Manager service
+ get _pwmgr() {
+ if (!this.__pwmgr)
+ this.__pwmgr = Cc["@mozilla.org/login-manager;1"].
+ getService(Ci.nsILoginManager);
+ return this.__pwmgr;
+ },
+
+ __promptService : null, // Prompt service for user interaction
+ get _promptService() {
+ if (!this.__promptService)
+ this.__promptService =
+ Cc["@mozilla.org/embedcomp/prompt-service;1"].
+ getService(Ci.nsIPromptService2);
+ return this.__promptService;
+ },
+
+ __strBundle : null, // String bundle for L10N
+ get _strBundle() {
+ if (!this.__strBundle) {
+ var bunService = Cc["@mozilla.org/intl/stringbundle;1"].
+ getService(Ci.nsIStringBundleService);
+ this.__strBundle = bunService.createBundle(
+ "chrome://passwordmgr/locale/passwordmgr.properties");
+ if (!this.__strBundle)
+ throw "String bundle for Login Manager not present!";
+ }
+
+ return this.__strBundle;
+ },
+
+ __brandBundle : null, // String bundle for L10N
+ get _brandBundle() {
+ if (!this.__brandBundle) {
+ var bunService = Cc["@mozilla.org/intl/stringbundle;1"].
+ getService(Ci.nsIStringBundleService);
+ this.__brandBundle = bunService.createBundle(
+ "chrome://branding/locale/brand.properties");
+ if (!this.__brandBundle)
+ throw "Branding string bundle not present!";
+ }
+
+ return this.__brandBundle;
+ },
+
+
+ __ellipsis : null,
+ get _ellipsis() {
+ if (!this.__ellipsis) {
+ this.__ellipsis = "\u2026";
+ try {
+ this.__ellipsis = Services.prefs.getComplexValue(
+ "intl.ellipsis", Ci.nsIPrefLocalizedString).data;
+ } catch (e) { }
+ }
+ return this.__ellipsis;
+ },
+
+
+ /*
+ * log
+ *
+ * Internal function for logging debug messages to the Error Console window.
+ */
+ log : function (message) {
+ if (!this._debug)
+ return;
+
+ dump("Pwmgr Prompter: " + message + "\n");
+ Services.console.logStringMessage("Pwmgr Prompter: " + message);
+ },
+
+
+ /* ---------- nsILoginManagerPrompter prompts ---------- */
+
+
+
+
+ /*
+ * init
+ *
+ */
+ init : function (aBrowser, aFactory) {
+ this._browser = aBrowser;
+ this._factory = aFactory || null;
+
+ var prefBranch = Services.prefs.getBranch("signon.");
+ this._debug = prefBranch.getBoolPref("debug");
+ this.log("===== initialized =====");
+ },
+
+
+ /*
+ * promptToSavePassword
+ *
+ */
+ promptToSavePassword : function (aLogin) {
+ var notifyBox = this._getNotifyBox();
+
+ if (notifyBox)
+ this._showSaveLoginNotification(notifyBox, aLogin);
+ else
+ this._showSaveLoginDialog(aLogin);
+ },
+
+
+ /*
+ * _showLoginNotification
+ *
+ * Displays a notification bar.
+ *
+ */
+ _showLoginNotification : function (aNotifyBox, aName, aText, aButtons) {
+ var oldBar = aNotifyBox.getNotificationWithValue(aName);
+ const priority = aNotifyBox.PRIORITY_INFO_MEDIUM;
+
+ this.log("Adding new " + aName + " notification bar");
+ var newBar = aNotifyBox.appendNotification(
+ aText, aName,
+ "chrome://mozapps/skin/passwordmgr/key.png",
+ priority, aButtons);
+
+ // The page we're going to hasn't loaded yet, so we want to persist
+ // across the first location change.
+ newBar.persistence++;
+
+ // Sites like Gmail perform a funky redirect dance before you end up
+ // at the post-authentication page. I don't see a good way to
+ // heuristically determine when to ignore such location changes, so
+ // we'll try ignoring location changes based on a time interval.
+ newBar.timeout = Date.now() + 20000; // 20 seconds
+
+ if (oldBar) {
+ this.log("(...and removing old " + aName + " notification bar)");
+ aNotifyBox.removeNotification(oldBar);
+ }
+ },
+
+
+ /*
+ * _showSaveLoginNotification
+ *
+ * Displays a notification bar (rather than a popup), to allow the user to
+ * save the specified login. This allows the user to see the results of
+ * their login, and only save a login which they know worked.
+ *
+ */
+ _showSaveLoginNotification : function (aNotifyBox, aLogin) {
+
+ // Ugh. We can't use the strings from the popup window, because they
+ // have the access key marked in the string (eg "Mo&zilla"), along
+ // with some weird rules for handling access keys that do not occur
+ // in the string, for L10N. See commonDialog.js's setLabelForNode().
+ var neverButtonText =
+ this._getLocalizedString("notifyBarNeverForSiteButtonText");
+ var neverButtonAccessKey =
+ this._getLocalizedString("notifyBarNeverForSiteButtonAccessKey");
+ var rememberButtonText =
+ this._getLocalizedString("notifyBarRememberButtonText");
+ var rememberButtonAccessKey =
+ this._getLocalizedString("notifyBarRememberButtonAccessKey");
+ var notNowButtonText =
+ this._getLocalizedString("notifyBarNotNowButtonText");
+ var notNowButtonAccessKey =
+ this._getLocalizedString("notifyBarNotNowButtonAccessKey");
+
+ var brandShortName =
+ this._brandBundle.GetStringFromName("brandShortName");
+ var displayHost = this._getShortDisplayHost(aLogin.hostname);
+ var notificationText;
+ if (aLogin.username) {
+ var displayUser = this._sanitizeUsername(aLogin.username);
+ notificationText = this._getLocalizedString(
+ "saveLoginText",
+ [brandShortName, displayUser, displayHost]);
+ } else {
+ notificationText = this._getLocalizedString(
+ "saveLoginTextNoUsername",
+ [brandShortName, displayHost]);
+ }
+
+ // The callbacks in |buttons| have a closure to access the variables
+ // in scope here; set one to |this._pwmgr| so we can get back to pwmgr
+ // without a getService() call.
+ var pwmgr = this._pwmgr;
+
+
+ var buttons = [
+ // "Remember" button
+ {
+ label: rememberButtonText,
+ accessKey: rememberButtonAccessKey,
+ popup: null,
+ callback: function(aNotificationBar, aButton) {
+ pwmgr.addLogin(aLogin);
+ }
+ },
+
+ // "Never for this site" button
+ {
+ label: neverButtonText,
+ accessKey: neverButtonAccessKey,
+ popup: null,
+ callback: function(aNotificationBar, aButton) {
+ pwmgr.setLoginSavingEnabled(aLogin.hostname, false);
+ }
+ },
+
+ // "Not now" button
+ {
+ label: notNowButtonText,
+ accessKey: notNowButtonAccessKey,
+ popup: null,
+ callback: function() { /* NOP */ }
+ }
+ ];
+
+ this._showLoginNotification(aNotifyBox, "password-save",
+ notificationText, buttons);
+ },
+
+
+ /*
+ * _showSaveLoginDialog
+ *
+ * Called when we detect a new login in a form submission,
+ * asks the user what to do.
+ *
+ */
+ _showSaveLoginDialog : function (aLogin) {
+ const buttonFlags = Ci.nsIPrompt.BUTTON_POS_1_DEFAULT +
+ (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_0) +
+ (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_1) +
+ (Ci.nsIPrompt.BUTTON_TITLE_IS_STRING * Ci.nsIPrompt.BUTTON_POS_2);
+
+ var brandShortName =
+ this._brandBundle.GetStringFromName("brandShortName");
+ var displayHost = this._getShortDisplayHost(aLogin.hostname);
+
+ var dialogText;
+ if (aLogin.username) {
+ var displayUser = this._sanitizeUsername(aLogin.username);
+ dialogText = this._getLocalizedString(
+ "saveLoginText",
+ [brandShortName, displayUser, displayHost]);
+ } else {
+ dialogText = this._getLocalizedString(
+ "saveLoginTextNoUsername",
+ [brandShortName, displayHost]);
+ }
+ var dialogTitle = this._getLocalizedString(
+ "savePasswordTitle");
+ var neverButtonText = this._getLocalizedString(
+ "neverForSiteButtonText");
+ var rememberButtonText = this._getLocalizedString(
+ "rememberButtonText");
+ var notNowButtonText = this._getLocalizedString(
+ "notNowButtonText");
+
+ this.log("Prompting user to save/ignore login");
+ var userChoice = this._promptService.confirmEx(null,
+ dialogTitle, dialogText,
+ buttonFlags, rememberButtonText,
+ notNowButtonText, neverButtonText,
+ null, {});
+ // Returns:
+ // 0 - Save the login
+ // 1 - Ignore the login this time
+ // 2 - Never save logins for this site
+ if (userChoice == 2) {
+ this.log("Disabling " + aLogin.hostname + " logins by request.");
+ this._pwmgr.setLoginSavingEnabled(aLogin.hostname, false);
+ } else if (userChoice == 0) {
+ this.log("Saving login for " + aLogin.hostname);
+ this._pwmgr.addLogin(aLogin);
+ } else {
+ // userChoice == 1 --> just ignore the login.
+ this.log("Ignoring login.");
+ }
+ },
+
+ /*
+ * promptToChangePassword
+ *
+ * Called when we think we detect a password change for an existing
+ * login, when the form being submitted contains multiple password
+ * fields.
+ *
+ */
+ promptToChangePassword : function (aOldLogin, aNewLogin) {
+ var notifyBox = this._getNotifyBox();
+
+ if (notifyBox)
+ this._showChangeLoginNotification(notifyBox, aOldLogin, aNewLogin.password);
+ else
+ this._showChangeLoginDialog(aOldLogin, aNewLogin.password);
+ },
+
+ /*
+ * _showChangeLoginNotification
+ *
+ * Shows the Change Password notification bar.
+ *
+ */
+ _showChangeLoginNotification : function (aNotifyBox, aOldLogin, aNewPassword) {
+ var notificationText;
+ if (aOldLogin.username)
+ notificationText = this._getLocalizedString(
+ "passwordChangeText",
+ [aOldLogin.username]);
+ else
+ notificationText = this._getLocalizedString(
+ "passwordChangeTextNoUser");
+
+ var changeButtonText =
+ this._getLocalizedString("notifyBarChangeButtonText");
+ var changeButtonAccessKey =
+ this._getLocalizedString("notifyBarChangeButtonAccessKey");
+ var dontChangeButtonText =
+ this._getLocalizedString("notifyBarDontChangeButtonText");
+ var dontChangeButtonAccessKey =
+ this._getLocalizedString("notifyBarDontChangeButtonAccessKey");
+
+ // The callbacks in |buttons| have a closure to access the variables
+ // in scope here; set one to |this._pwmgr| so we can get back to pwmgr
+ // without a getService() call.
+ var self = this;
+
+ var buttons = [
+ // "Yes" button
+ {
+ label: changeButtonText,
+ accessKey: changeButtonAccessKey,
+ popup: null,
+ callback: function(aNotificationBar, aButton) {
+ self._updateLogin(aOldLogin, aNewPassword);
+ }
+ },
+
+ // "No" button
+ {
+ label: dontChangeButtonText,
+ accessKey: dontChangeButtonAccessKey,
+ popup: null,
+ callback: function(aNotificationBar, aButton) {
+ // do nothing
+ }
+ }
+ ];
+
+ this._showLoginNotification(aNotifyBox, "password-change",
+ notificationText, buttons);
+ },
+
+ /*
+ * _showChangeLoginDialog
+ *
+ * Shows the Change Password dialog.
+ *
+ */
+ _showChangeLoginDialog : function (aOldLogin, aNewPassword) {
+ const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS;
+
+ var dialogText;
+ if (aOldLogin.username)
+ dialogText = this._getLocalizedString(
+ "passwordChangeText",
+ [aOldLogin.username]);
+ else
+ dialogText = this._getLocalizedString(
+ "passwordChangeTextNoUser");
+
+ var dialogTitle = this._getLocalizedString(
+ "passwordChangeTitle");
+
+ // returns 0 for yes, 1 for no.
+ var ok = !this._promptService.confirmEx(null,
+ dialogTitle, dialogText, buttonFlags,
+ null, null, null,
+ null, {});
+ if (ok) {
+ this.log("Updating password for user " + aOldLogin.username);
+ this._updateLogin(aOldLogin, aNewPassword);
+ }
+ },
+
+
+ /*
+ * promptToChangePasswordWithUsernames
+ *
+ * Called when we detect a password change in a form submission, but we
+ * don't know which existing login (username) it's for. Asks the user
+ * to select a username and confirm the password change.
+ *
+ * Note: The caller doesn't know the username for aNewLogin, so this
+ * function fills in .username and .usernameField with the values
+ * from the login selected by the user.
+ *
+ * Note; XPCOM stupidity: |count| is just |logins.length|.
+ */
+ promptToChangePasswordWithUsernames : function (logins, count, aNewLogin) {
+ const buttonFlags = Ci.nsIPrompt.STD_YES_NO_BUTTONS;
+
+ var usernames = logins.map(function (l) l.username);
+ var dialogText = this._getLocalizedString("userSelectText");
+ var dialogTitle = this._getLocalizedString("passwordChangeTitle");
+ var selectedIndex = { value: null };
+
+ // If user selects ok, outparam.value is set to the index
+ // of the selected username.
+ var ok = this._promptService.select(null,
+ dialogTitle, dialogText,
+ usernames.length, usernames,
+ selectedIndex);
+ if (ok) {
+ // Now that we know which login to use, modify its password.
+ var selectedLogin = logins[selectedIndex.value];
+ this.log("Updating password for user " + selectedLogin.username);
+ this._updateLogin(selectedLogin, aNewLogin.password);
+ }
+ },
+
+
+
+
+ /* ---------- Internal Methods ---------- */
+
+
+
+
+ /*
+ * _updateLogin
+ */
+ _updateLogin : function (login, newPassword) {
+ var now = Date.now();
+ var propBag = Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(Ci.nsIWritablePropertyBag);
+ if (newPassword) {
+ propBag.setProperty("password", newPassword);
+ // Explicitly set the password change time here (even though it would
+ // be changed automatically), to ensure that it's exactly the same
+ // value as timeLastUsed.
+ propBag.setProperty("timePasswordChanged", now);
+ }
+ propBag.setProperty("timeLastUsed", now);
+ propBag.setProperty("timesUsedIncrement", 1);
+ this._pwmgr.modifyLogin(login, propBag);
+ },
+
+ /*
+ * _getNotifyBox
+ *
+ * Returns the notification box to this prompter, or null if there isn't
+ * a notification box available.
+ */
+ _getNotifyBox : function () {
+ let notifyBox = null;
+ try {
+ let chromeWin = this._browser.ownerDocument.defaultView;
+ if (chromeWin.getNotificationBox) {
+ notifyBox = chromeWin.getNotificationBox(this._browser);
+ } else {
+ this.log("getNotificationBox() not available on window");
+ }
+
+ } catch (e) {
+ // If any errors happen, just assume no notification box.
+ this.log("No notification box available: " + e)
+ }
+ return notifyBox;
+ },
+
+
+ /*
+ * _getLocalizedString
+ *
+ * Can be called as:
+ * _getLocalizedString("key1");
+ * _getLocalizedString("key2", ["arg1"]);
+ * _getLocalizedString("key3", ["arg1", "arg2"]);
+ * (etc)
+ *
+ * Returns the localized string for the specified key,
+ * formatted if required.
+ *
+ */
+ _getLocalizedString : function (key, formatArgs) {
+ if (formatArgs)
+ return this._strBundle.formatStringFromName(
+ key, formatArgs, formatArgs.length);
+ else
+ return this._strBundle.GetStringFromName(key);
+ },
+
+
+ /*
+ * _sanitizeUsername
+ *
+ * Sanitizes the specified username, by stripping quotes and truncating if
+ * it's too long. This helps prevent an evil site from messing with the
+ * "save password?" prompt too much.
+ */
+ _sanitizeUsername : function (username) {
+ if (username.length > 30) {
+ username = username.substring(0, 30);
+ username += this._ellipsis;
+ }
+ return username.replace(/['"]/g, "");
+ },
+
+
+ /*
+ * _getFormattedHostname
+ *
+ * The aURI parameter may either be a string uri, or an nsIURI instance.
+ *
+ * Returns the hostname to use in a nsILoginInfo object (for example,
+ * "http://example.com").
+ */
+ _getFormattedHostname : function (aURI) {
+ var uri;
+ if (aURI instanceof Ci.nsIURI) {
+ uri = aURI;
+ } else {
+ uri = Services.io.newURI(aURI, null, null);
+ }
+ var scheme = uri.scheme;
+
+ var hostname = scheme + "://" + uri.host;
+
+ // If the URI explicitly specified a port, only include it when
+ // it's not the default. (We never want "http://foo.com:80")
+ port = uri.port;
+ if (port != -1) {
+ var handler = Services.io.getProtocolHandler(scheme);
+ if (port != handler.defaultPort)
+ hostname += ":" + port;
+ }
+
+ return hostname;
+ },
+
+
+ /*
+ * _getShortDisplayHost
+ *
+ * Converts a login's hostname field (a URL) to a short string for
+ * prompting purposes. Eg, "http://foo.com" --> "foo.com", or
+ * "ftp://www.site.co.uk" --> "site.co.uk".
+ */
+ _getShortDisplayHost: function (aURIString) {
+ var displayHost;
+
+ var eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"].
+ getService(Ci.nsIEffectiveTLDService);
+ var idnService = Cc["@mozilla.org/network/idn-service;1"].
+ getService(Ci.nsIIDNService);
+ try {
+ var uri = Services.io.newURI(aURIString, null, null);
+ var baseDomain = eTLDService.getBaseDomain(uri);
+ displayHost = idnService.convertToDisplayIDN(baseDomain, {});
+ } catch (e) {
+ this.log("_getShortDisplayHost couldn't process " + aURIString);
+ }
+
+ if (!displayHost)
+ displayHost = aURIString;
+
+ return displayHost;
+ },
+
+}; // end of LoginManagerPrompter implementation
+
+
+var component = [LoginManagerPrompter];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(component);
+
diff --git a/browser/metro/components/Makefile.in b/browser/metro/components/Makefile.in
new file mode 100644
index 000000000000..1ebcbb650cd0
--- /dev/null
+++ b/browser/metro/components/Makefile.in
@@ -0,0 +1,53 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+include $(topsrcdir)/config/config.mk
+
+# metro/components.manifest
+MODULE = components
+XPIDL_MODULE = components
+
+XPIDLSRCS = \
+ SessionStore.idl \
+ LoginManagerPrompter.idl \
+ $(NULL)
+
+EXTRA_PP_COMPONENTS = \
+ components.manifest \
+ AboutRedirector.js \
+ BrowserCLH.js \
+ BrowserStartup.js \
+ DirectoryProvider.js\
+ HelperAppDialog.js \
+ Sidebar.js \
+ SessionStore.js \
+ $(NULL)
+
+EXTRA_COMPONENTS = \
+ AlertsService.js \
+ ContentPermissionPrompt.js \
+ DownloadManagerUI.js \
+ PromptService.js \
+ ContentDispatchChooser.js \
+ FormAutoComplete.js \
+ LoginManager.js \
+ LoginManagerPrompter.js \
+ CapturePicker.js \
+ $(NULL)
+
+ifdef MOZ_SAFE_BROWSING
+EXTRA_COMPONENTS += SafeBrowsing.js
+endif
+
+ifdef MOZ_UPDATER
+EXTRA_COMPONENTS += UpdatePrompt.js
+endif
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/metro/components/PromptService.js b/browser/metro/components/PromptService.js
new file mode 100644
index 000000000000..66d21c0c196c
--- /dev/null
+++ b/browser/metro/components/PromptService.js
@@ -0,0 +1,917 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+const Ci = Components.interfaces;
+const Cc = Components.classes;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+// Whitelist of methods we remote - to check against malicious data.
+// For example, it would be dangerous to allow content to show auth prompts.
+const REMOTABLE_METHODS = {
+ alert: { outParams: [] },
+ alertCheck: { outParams: [4] },
+ confirm: { outParams: [] },
+ prompt: { outParams: [3, 5] },
+ confirmEx: { outParams: [8] },
+ confirmCheck: { outParams: [4] },
+ select: { outParams: [5] }
+};
+
+var gPromptService = null;
+
+function PromptService() {
+ // Depending on if we are in the parent or child, prepare to remote
+ // certain calls
+ var appInfo = Cc["@mozilla.org/xre/app-info;1"];
+ if (!appInfo || appInfo.getService(Ci.nsIXULRuntime).processType == Ci.nsIXULRuntime.PROCESS_TYPE_DEFAULT) {
+ // Parent process
+ this.inContentProcess = false;
+
+ // Used for wakeups service. FIXME: clean up with bug 593407
+ this.wrappedJSObject = this;
+
+ // Setup listener for child messages. We don't need to call
+ // addMessageListener as the wakeup service will do that for us.
+ this.receiveMessage = function(aMessage) {
+ var json = aMessage.json;
+ switch (aMessage.name) {
+ case "Prompt:Call":
+ var method = aMessage.json.method;
+ if (!REMOTABLE_METHODS.hasOwnProperty(method))
+ throw "PromptServiceRemoter received an invalid method "+method;
+
+ var arguments = aMessage.json.arguments;
+ var ret = this[method].apply(this, arguments);
+ // Return multiple return values in objects of form { value: ... },
+ // and also with the actual return value at the end
+ arguments.push(ret);
+ return arguments;
+ }
+ };
+ } else {
+ // Child process
+ this.inContentProcess = true;
+ }
+
+ gPromptService = this;
+}
+
+PromptService.prototype = {
+ classID: Components.ID("{9a61149b-2276-4a0a-b79c-be994ad106cf}"),
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPromptFactory, Ci.nsIPromptService, Ci.nsIPromptService2]),
+
+ /* ---------- nsIPromptFactory ---------- */
+
+ // XXX Copied from nsPrompter.js.
+ getPrompt: function getPrompt(domWin, iid) {
+ if (this.inContentProcess)
+ return ContentPrompt.QueryInterface(iid);
+
+ let doc = this.getDocument();
+ if (!doc) {
+ let fallback = this._getFallbackService();
+ return fallback.getPrompt(domWin, iid);
+ }
+
+ let p = new Prompt(domWin, doc);
+ p.QueryInterface(iid);
+ return p;
+ },
+
+ /* ---------- private memebers ---------- */
+
+ _getFallbackService: function _getFallbackService() {
+ return Components.classesByID["{7ad1b327-6dfa-46ec-9234-f2a620ea7e00}"]
+ .getService(Ci.nsIPromptService);
+ },
+
+ getDocument: function getDocument() {
+ let win = Services.wm.getMostRecentWindow("navigator:browser");
+ return win ? win.document : null;
+ },
+
+ // nsIPromptService and nsIPromptService2 methods proxy to our Prompt class
+ // if we can show in-document popups, or to the fallback service otherwise.
+ callProxy: function(aMethod, aArguments) {
+ let prompt;
+ if (this.inContentProcess) {
+ // Bring this tab to the front, so prompt appears on the right tab
+ var window = aArguments[0];
+ if (window && window.document) {
+ var event = window.document.createEvent("Events");
+ event.initEvent("DOMWillOpenModalDialog", true, true);
+ let winUtils = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ winUtils.dispatchEventToChromeOnly(window, event);
+ }
+ prompt = ContentPrompt;
+ } else {
+ let doc = this.getDocument();
+ if (!doc) {
+ let fallback = this._getFallbackService();
+ return fallback[aMethod].apply(fallback, aArguments);
+ }
+ let domWin = aArguments[0];
+ prompt = new Prompt(domWin, doc);
+ }
+ return prompt[aMethod].apply(prompt, Array.prototype.slice.call(aArguments, 1));
+ },
+
+ /* ---------- nsIPromptService ---------- */
+
+ alert: function() {
+ return this.callProxy("alert", arguments);
+ },
+ alertCheck: function() {
+ return this.callProxy("alertCheck", arguments);
+ },
+ confirm: function() {
+ return this.callProxy("confirm", arguments);
+ },
+ confirmCheck: function() {
+ return this.callProxy("confirmCheck", arguments);
+ },
+ confirmEx: function() {
+ return this.callProxy("confirmEx", arguments);
+ },
+ prompt: function() {
+ return this.callProxy("prompt", arguments);
+ },
+ promptUsernameAndPassword: function() {
+ return this.callProxy("promptUsernameAndPassword", arguments);
+ },
+ promptPassword: function() {
+ return this.callProxy("promptPassword", arguments);
+ },
+ select: function() {
+ return this.callProxy("select", arguments);
+ },
+
+ /* ---------- nsIPromptService2 ---------- */
+
+ promptAuth: function() {
+ return this.callProxy("promptAuth", arguments);
+ },
+ asyncPromptAuth: function() {
+ return this.callProxy("asyncPromptAuth", arguments);
+ }
+};
+
+// Implementation of nsIPrompt that just forwards to the parent process.
+let ContentPrompt = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt]),
+
+ sendMessage: function sendMessage(aMethod) {
+ let args = Array.prototype.slice.call(arguments);
+ args[0] = null; // No need to pass "window" argument to the prompt service.
+
+ // We send all prompts as sync, even alert (which has no important
+ // return value), as otherwise program flow will continue, and the
+ // script can theoretically show several alerts at once. In particular
+ // this can lead to a bug where you cannot click the earlier one, which
+ // is now hidden by a new one (and Fennec is helplessly frozen).
+ var json = { method: aMethod, arguments: args };
+ var response = this.messageManager.sendSyncMessage("Prompt:Call", json)[0];
+
+ // Args copying - for methods that have out values
+ REMOTABLE_METHODS[aMethod].outParams.forEach(function(i) {
+ args[i].value = response[i].value;
+ });
+ return response.pop(); // final return value was given at the end
+ }
+};
+
+XPCOMUtils.defineLazyServiceGetter(ContentPrompt, "messageManager",
+ "@mozilla.org/childprocessmessagemanager;1", Ci.nsISyncMessageSender);
+
+// Add remotable methods to ContentPrompt.
+for (let [method, _] in Iterator(REMOTABLE_METHODS)) {
+ ContentPrompt[method] = ContentPrompt.sendMessage.bind(ContentPrompt, method);
+}
+
+function Prompt(aDomWin, aDocument) {
+ this._domWin = aDomWin;
+ this._doc = aDocument;
+}
+
+Prompt.prototype = {
+ _domWin: null,
+ _doc: null,
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt, Ci.nsIAuthPrompt, Ci.nsIAuthPrompt2]),
+
+ /* ---------- internal methods ---------- */
+
+ openDialog: function openDialog(aSrc, aParams) {
+ let browser = Services.wm.getMostRecentWindow("navigator:browser");
+ return browser.DialogUI.importModal(this._domWin, aSrc, aParams);
+ },
+
+ _setupPrompt: function setupPrompt(aDoc, aType, aTitle, aText, aCheck) {
+ aDoc.getElementById("prompt-" + aType + "-title").appendChild(aDoc.createTextNode(aTitle));
+ aDoc.getElementById("prompt-" + aType + "-message").appendChild(aDoc.createTextNode(aText));
+
+ if (aCheck && aCheck.msg) {
+ aDoc.getElementById("prompt-" + aType + "-checkbox").checked = aCheck.value;
+ this.setLabelForNode(aDoc.getElementById("prompt-" + aType + "-checkbox-label"), aCheck.msg);
+ aDoc.getElementById("prompt-" + aType + "-checkbox").removeAttribute("collapsed");
+ }
+ },
+
+ commonPrompt: function commonPrompt(aTitle, aText, aValue, aCheckMsg, aCheckState, isPassword) {
+ var params = new Object();
+ params.result = false;
+ params.checkbox = aCheckState;
+ params.value = aValue;
+
+ let dialog = this.openDialog("chrome://browser/content/prompt/prompt.xul", params);
+ let doc = this._doc;
+ this._setupPrompt(doc, "prompt", aTitle, aText, {value: aCheckState.value, msg: aCheckMsg});
+ doc.getElementById("prompt-prompt-textbox").value = aValue.value;
+ if (isPassword)
+ doc.getElementById("prompt-prompt-textbox").type = "password";
+
+ dialog.waitForClose();
+ return params.result;
+ },
+
+ //
+ // Copied from chrome://global/content/commonDialog.js
+ //
+ setLabelForNode: function setLabelForNode(aNode, aLabel) {
+ // This is for labels which may contain embedded access keys.
+ // If we end in (&X) where X represents the access key, optionally preceded
+ // by spaces and/or followed by the ':' character, store the access key and
+ // remove the access key placeholder + leading spaces from the label.
+ // Otherwise a character preceded by one but not two &s is the access key.
+ // Store it and remove the &.
+
+ // Note that if you change the following code, see the comment of
+ // nsTextBoxFrame::UpdateAccessTitle.
+
+ if (!aLabel)
+ return;
+
+ var accessKey = null;
+ if (/ *\(\&([^&])\)(:)?$/.test(aLabel)) {
+ aLabel = RegExp.leftContext + RegExp.$2;
+ accessKey = RegExp.$1;
+ } else if (/^(.*[^&])?\&(([^&]).*$)/.test(aLabel)) {
+ aLabel = RegExp.$1 + RegExp.$2;
+ accessKey = RegExp.$3;
+ }
+
+ // && is the magic sequence to embed an & in your label.
+ aLabel = aLabel.replace(/\&\&/g, "&");
+ if (aNode instanceof Ci.nsIDOMXULLabelElement) {
+ aNode.setAttribute("value", aLabel);
+ } else if (aNode instanceof Ci.nsIDOMXULDescriptionElement) {
+ let text = aNode.ownerDocument.createTextNode(aLabel);
+ aNode.appendChild(text);
+ } else { // Set text for other xul elements
+ aNode.setAttribute("label", aLabel);
+ }
+
+ // XXXjag bug 325251
+ // Need to set this after aNode.setAttribute("value", aLabel);
+ if (accessKey)
+ aNode.setAttribute("accesskey", accessKey);
+ },
+
+ /*
+ * ---------- interface disambiguation ----------
+ *
+ * XXX Copied from nsPrompter.js.
+ *
+ * nsIPrompt and nsIAuthPrompt share 3 method names with slightly
+ * different arguments. All but prompt() have the same number of
+ * arguments, so look at the arg types to figure out how we're being
+ * called. :-(
+ */
+ prompt: function prompt() {
+ if (gPromptService.inContentProcess)
+ return gPromptService.callProxy("prompt", [null].concat(Array.prototype.slice.call(arguments)));
+
+ // also, the nsIPrompt flavor has 5 args instead of 6.
+ if (typeof arguments[2] == "object")
+ return this.nsIPrompt_prompt.apply(this, arguments);
+ else
+ return this.nsIAuthPrompt_prompt.apply(this, arguments);
+ },
+
+ promptUsernameAndPassword: function promptUsernameAndPassword() {
+ // Both have 6 args, so use types.
+ if (typeof arguments[2] == "object")
+ return this.nsIPrompt_promptUsernameAndPassword.apply(this, arguments);
+ else
+ return this.nsIAuthPrompt_promptUsernameAndPassword.apply(this, arguments);
+ },
+
+ promptPassword: function promptPassword() {
+ // Both have 5 args, so use types.
+ if (typeof arguments[2] == "object")
+ return this.nsIPrompt_promptPassword.apply(this, arguments);
+ else
+ return this.nsIAuthPrompt_promptPassword.apply(this, arguments);
+ },
+
+ /* ---------- nsIPrompt ---------- */
+
+ alert: function alert(aTitle, aText) {
+ let dialog = this.openDialog("chrome://browser/content/prompt/alert.xul", null);
+ let doc = this._doc;
+ this._setupPrompt(doc, "alert", aTitle, aText);
+
+ dialog.waitForClose();
+ },
+
+ alertCheck: function alertCheck(aTitle, aText, aCheckMsg, aCheckState) {
+ let dialog = this.openDialog("chrome://browser/content/prompt/alert.xul", aCheckState);
+ let doc = this._doc;
+ this._setupPrompt(doc, "alert", aTitle, aText, {value: aCheckState.value, msg: aCheckMsg});
+ dialog.waitForClose();
+ },
+
+ confirm: function confirm(aTitle, aText) {
+ var params = new Object();
+ params.result = false;
+
+ let dialog = this.openDialog("chrome://browser/content/prompt/confirm.xul", params);
+ let doc = this._doc;
+ this._setupPrompt(doc, "confirm", aTitle, aText);
+
+ dialog.waitForClose();
+ return params.result;
+ },
+
+ confirmCheck: function confirmCheck(aTitle, aText, aCheckMsg, aCheckState) {
+ var params = new Object();
+ params.result = false;
+ params.checkbox = aCheckState;
+
+ let dialog = this.openDialog("chrome://browser/content/prompt/confirm.xul", params);
+ let doc = this._doc;
+ this._setupPrompt(doc, "confirm", aTitle, aText, {value: aCheckState.value, msg: aCheckMsg});
+
+ dialog.waitForClose();
+ return params.result;
+ },
+
+ confirmEx: function confirmEx(aTitle, aText, aButtonFlags, aButton0,
+ aButton1, aButton2, aCheckMsg, aCheckState) {
+
+ let numButtons = 0;
+ let titles = [aButton0, aButton1, aButton2];
+
+ let defaultButton = 0;
+ if (aButtonFlags & Ci.nsIPromptService.BUTTON_POS_1_DEFAULT)
+ defaultButton = 1;
+ if (aButtonFlags & Ci.nsIPromptService.BUTTON_POS_2_DEFAULT)
+ defaultButton = 2;
+
+ var params = {
+ result: false,
+ checkbox: aCheckState,
+ defaultButton: defaultButton
+ }
+
+ let dialog = this.openDialog("chrome://browser/content/prompt/confirm.xul", params);
+ let doc = this._doc;
+ this._setupPrompt(doc, "confirm", aTitle, aText, {value: aCheckState.value, msg: aCheckMsg});
+
+ let bbox = doc.getElementById("prompt-confirm-buttons-box");
+ while (bbox.lastChild)
+ bbox.removeChild(bbox.lastChild);
+
+ for (let i = 0; i < 3; i++) {
+ let bTitle = null;
+ switch (aButtonFlags & 0xff) {
+ case Ci.nsIPromptService.BUTTON_TITLE_OK :
+ bTitle = PromptUtils.getLocaleString("OK");
+ break;
+ case Ci.nsIPromptService.BUTTON_TITLE_CANCEL :
+ bTitle = PromptUtils.getLocaleString("Cancel");
+ break;
+ case Ci.nsIPromptService.BUTTON_TITLE_YES :
+ bTitle = PromptUtils.getLocaleString("Yes");
+ break;
+ case Ci.nsIPromptService.BUTTON_TITLE_NO :
+ bTitle = PromptUtils.getLocaleString("No");
+ break;
+ case Ci.nsIPromptService.BUTTON_TITLE_SAVE :
+ bTitle = PromptUtils.getLocaleString("Save");
+ break;
+ case Ci.nsIPromptService.BUTTON_TITLE_DONT_SAVE :
+ bTitle = PromptUtils.getLocaleString("DontSave");
+ break;
+ case Ci.nsIPromptService.BUTTON_TITLE_REVERT :
+ bTitle = PromptUtils.getLocaleString("Revert");
+ break;
+ case Ci.nsIPromptService.BUTTON_TITLE_IS_STRING :
+ bTitle = titles[i];
+ break;
+ }
+
+ if (bTitle) {
+ let button = doc.createElement("button");
+ button.className = "prompt-button";
+ this.setLabelForNode(button, bTitle);
+ if (i == defaultButton) {
+ button.setAttribute("command", "cmd_ok");
+ }
+ else {
+ button.setAttribute("oncommand",
+ "document.getElementById('prompt-confirm-dialog').PromptHelper.closeConfirm(" + i + ")");
+ }
+ bbox.appendChild(button);
+ }
+
+ aButtonFlags >>= 8;
+ }
+
+ dialog.waitForClose();
+ return params.result;
+ },
+
+ nsIPrompt_prompt: function nsIPrompt_prompt(aTitle, aText, aValue, aCheckMsg, aCheckState) {
+ return this.commonPrompt(aTitle, aText, aValue, aCheckMsg, aCheckState, false);
+ },
+
+ nsIPrompt_promptPassword: function nsIPrompt_promptPassword(
+ aTitle, aText, aPassword, aCheckMsg, aCheckState) {
+ return this.commonPrompt(aTitle, aText, aPassword, aCheckMsg, aCheckState, true);
+ },
+
+ nsIPrompt_promptUsernameAndPassword: function nsIPrompt_promptUsernameAndPassword(
+ aTitle, aText, aUsername, aPassword, aCheckMsg, aCheckState) {
+ var params = new Object();
+ params.result = false;
+ params.checkbox = aCheckState;
+ params.user = aUsername;
+ params.password = aPassword;
+
+ let dialog = this.openDialog("chrome://browser/content/prompt/promptPassword.xul", params);
+ let doc = this._doc;
+ this._setupPrompt(doc, "password", aTitle, aText, {value: aCheckState.value, msg: aCheckMsg});
+
+ doc.getElementById("prompt-password-user").value = aUsername.value;
+ doc.getElementById("prompt-password-password").value = aPassword.value;
+
+ dialog.waitForClose();
+ return params.result;
+ },
+
+ select: function select(aTitle, aText, aCount, aSelectList, aOutSelection) {
+ var params = new Object();
+ params.result = false;
+ params.selection = aOutSelection;
+
+ let dialog = this.openDialog("chrome://browser/content/prompt/select.xul", params);
+ let doc = this._doc;
+ this._setupPrompt(doc, "select", aTitle, aText);
+
+ let list = doc.getElementById("prompt-select-list");
+ for (let i = 0; i < aCount; i++)
+ list.appendItem(aSelectList[i], null, null);
+
+ // select the first one
+ list.selectedIndex = 0;
+
+ dialog.waitForClose();
+ return params.result;
+ },
+
+ /* ---------- nsIAuthPrompt ---------- */
+
+ nsIAuthPrompt_prompt : function (title, text, passwordRealm, savePassword, defaultText, result) {
+ // TODO: Port functions from nsLoginManagerPrompter.js to here
+ if (defaultText)
+ result.value = defaultText;
+ return this.nsIPrompt_prompt(title, text, result, null, {});
+ },
+
+ nsIAuthPrompt_promptUsernameAndPassword : function (aTitle, aText, aPasswordRealm, aSavePassword, aUser, aPass) {
+ return nsIAuthPrompt_loginPrompt(aTitle, aText, aPasswordRealm, aSavePassword, aUser, aPass);
+ },
+
+ nsIAuthPrompt_promptPassword : function (aTitle, aText, aPasswordRealm, aSavePassword, aPass) {
+ return nsIAuthPrompt_loginPrompt(aTitle, aText, aPasswordRealm, aSavePassword, null, aPass);
+ },
+
+ nsIAuthPrompt_loginPrompt: function(aTitle, aPasswordRealm, aSavePassword, aUser, aPass) {
+ let checkMsg = null;
+ let check = { value: false };
+ let [hostname, realm, aUser] = PromptUtils.getHostnameAndRealm(aPasswordRealm);
+
+ let canSave = PromptUtils.canSaveLogin(hostname, aSavePassword);
+ if (canSave) {
+ // Look for existing logins.
+ let foundLogins = PromptUtils.pwmgr.findLogins({}, hostname, null, realm);
+ [checkMsg, check] = PromptUtils.getUsernameAndPassword(foundLogins, aUser, aPass);
+ }
+
+ let ok = false;
+ if (aUser)
+ ok = this.nsIPrompt_promptUsernameAndPassword(aTitle, aText, aUser, aPass, checkMsg, check);
+ else
+ ok = this.nsIPrompt_promptPassword(aTitle, aText, aPass, checkMsg, check);
+
+ if (ok && canSave && check.value)
+ PromptUtils.savePassword(hostname, realm, aUser, aPass);
+
+ return ok; },
+
+ /* ---------- nsIAuthPrompt2 ---------- */
+
+ promptAuth: function promptAuth(aChannel, aLevel, aAuthInfo) {
+ let checkMsg = null;
+ let check = { value: false };
+ let message = PromptUtils.makeDialogText(aChannel, aAuthInfo);
+ let [username, password] = PromptUtils.getAuthInfo(aAuthInfo);
+ let [hostname, httpRealm] = PromptUtils.getAuthTarget(aChannel, aAuthInfo);
+ let foundLogins = PromptUtils.pwmgr.findLogins({}, hostname, null, httpRealm);
+
+ let canSave = PromptUtils.canSaveLogin(hostname, null);
+ if (canSave)
+ [checkMsg, check] = PromptUtils.getUsernameAndPassword(foundLogins, username, password);
+
+ if (username.value && password.value) {
+ PromptUtils.setAuthInfo(aAuthInfo, username.value, password.value);
+ }
+
+ let canAutologin = false;
+ if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY &&
+ !(aAuthInfo.flags & Ci.nsIAuthInformation.PREVIOUS_FAILED) &&
+ Services.prefs.getBoolPref("signon.autologin.proxy"))
+ canAutologin = true;
+
+ let ok = canAutologin;
+ if (!ok && aAuthInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD)
+ ok = this.nsIPrompt_promptPassword(null, message, password, checkMsg, check);
+ else if (!ok)
+ ok = this.nsIPrompt_promptUsernameAndPassword(null, message, username, password, checkMsg, check);
+
+ PromptUtils.setAuthInfo(aAuthInfo, username.value, password.value);
+
+ if (ok && canSave && check.value)
+ PromptUtils.savePassword(foundLogins, username, password, hostname, httpRealm);
+
+ return ok;
+ },
+
+ _asyncPrompts: {},
+ _asyncPromptInProgress: false,
+
+ _doAsyncPrompt : function() {
+ if (this._asyncPromptInProgress)
+ return;
+
+ // Find the first prompt key we have in the queue
+ let hashKey = null;
+ for (hashKey in this._asyncPrompts)
+ break;
+
+ if (!hashKey)
+ return;
+
+ // If login manger has logins for this host, defer prompting if we're
+ // already waiting on a master password entry.
+ let prompt = this._asyncPrompts[hashKey];
+ let prompter = prompt.prompter;
+ let [hostname, httpRealm] = PromptUtils.getAuthTarget(prompt.channel, prompt.authInfo);
+ let foundLogins = PromptUtils.pwmgr.findLogins({}, hostname, null, httpRealm);
+ if (foundLogins.length > 0 && PromptUtils.pwmgr.uiBusy)
+ return;
+
+ this._asyncPromptInProgress = true;
+ prompt.inProgress = true;
+
+ let self = this;
+
+ let runnable = {
+ run: function() {
+ let ok = false;
+ try {
+ ok = prompter.promptAuth(prompt.channel, prompt.level, prompt.authInfo);
+ } catch (e) {
+ Cu.reportError("_doAsyncPrompt:run: " + e + "\n");
+ }
+
+ delete self._asyncPrompts[hashKey];
+ prompt.inProgress = false;
+ self._asyncPromptInProgress = false;
+
+ for each (let consumer in prompt.consumers) {
+ if (!consumer.callback)
+ // Not having a callback means that consumer didn't provide it
+ // or canceled the notification
+ continue;
+
+ try {
+ if (ok)
+ consumer.callback.onAuthAvailable(consumer.context, prompt.authInfo);
+ else
+ consumer.callback.onAuthCancelled(consumer.context, true);
+ } catch (e) { /* Throw away exceptions caused by callback */ }
+ }
+ self._doAsyncPrompt();
+ }
+ }
+
+ Services.tm.mainThread.dispatch(runnable, Ci.nsIThread.DISPATCH_NORMAL);
+ },
+
+ asyncPromptAuth: function asyncPromptAuth(aChannel, aCallback, aContext, aLevel, aAuthInfo) {
+ let cancelable = null;
+ try {
+ // If the user submits a login but it fails, we need to remove the
+ // notification bar that was displayed. Conveniently, the user will
+ // be prompted for authentication again, which brings us here.
+ //this._removeLoginNotifications();
+
+ cancelable = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsICancelable]),
+ callback: aCallback,
+ context: aContext,
+ cancel: function() {
+ this.callback.onAuthCancelled(this.context, false);
+ this.callback = null;
+ this.context = null;
+ }
+ };
+ let [hostname, httpRealm] = PromptUtils.getAuthTarget(aChannel, aAuthInfo);
+ let hashKey = aLevel + "|" + hostname + "|" + httpRealm;
+ let asyncPrompt = this._asyncPrompts[hashKey];
+ if (asyncPrompt) {
+ asyncPrompt.consumers.push(cancelable);
+ return cancelable;
+ }
+
+ asyncPrompt = {
+ consumers: [cancelable],
+ channel: aChannel,
+ authInfo: aAuthInfo,
+ level: aLevel,
+ inProgress : false,
+ prompter: this
+ }
+
+ this._asyncPrompts[hashKey] = asyncPrompt;
+ this._doAsyncPrompt();
+ } catch (e) {
+ Cu.reportError("PromptService: " + e + "\n");
+ throw e;
+ }
+ return cancelable;
+ }
+};
+
+let PromptUtils = {
+ getLocaleString: function pu_getLocaleString(aKey, aService) {
+ if (aService == "passwdmgr")
+ return this.passwdBundle.GetStringFromName(aKey);
+
+ return this.bundle.GetStringFromName(aKey);
+ },
+
+ get pwmgr() {
+ delete this.pwmgr;
+ return this.pwmgr = Cc["@mozilla.org/login-manager;1"].getService(Ci.nsILoginManager);
+ },
+
+ getHostnameAndRealm: function pu_getHostnameAndRealm(aRealmString) {
+ let httpRealm = /^.+ \(.+\)$/;
+ if (httpRealm.test(aRealmString))
+ return [null, null, null];
+
+ let uri = Services.io.newURI(aRealmString, null, null);
+ let pathname = "";
+
+ if (uri.path != "/")
+ pathname = uri.path;
+
+ let formattedHostname = this._getFormattedHostname(uri);
+ return [formattedHostname, formattedHostname + pathname, uri.username];
+ },
+
+ canSaveLogin: function pu_canSaveLogin(aHostname, aSavePassword) {
+ let canSave = !this._inPrivateBrowsing && this.pwmgr.getLoginSavingEnabled(aHostname)
+ if (aSavePassword)
+ canSave = canSave && (aSavePassword == Ci.nsIAuthPrompt.SAVE_PASSWORD_PERMANENTLY)
+ return canSave;
+ },
+
+ getUsernameAndPassword: function pu_getUsernameAndPassword(aFoundLogins, aUser, aPass) {
+ let checkLabel = null;
+ let check = { value: false };
+ let selectedLogin;
+
+ checkLabel = this.getLocaleString("rememberPassword", "passwdmgr");
+
+ // XXX Like the original code, we can't deal with multiple
+ // account selection. (bug 227632)
+ if (aFoundLogins.length > 0) {
+ selectedLogin = aFoundLogins[0];
+
+ // If the caller provided a username, try to use it. If they
+ // provided only a password, this will try to find a password-only
+ // login (or return null if none exists).
+ if (aUser.value)
+ selectedLogin = this.findLogin(aFoundLogins, "username", aUser.value);
+
+ if (selectedLogin) {
+ check.value = true;
+ aUser.value = selectedLogin.username;
+ // If the caller provided a password, prefer it.
+ if (!aPass.value)
+ aPass.value = selectedLogin.password;
+ }
+ }
+
+ return [checkLabel, check];
+ },
+
+ findLogin: function pu_findLogin(aLogins, aName, aValue) {
+ for (let i = 0; i < aLogins.length; i++)
+ if (aLogins[i][aName] == aValue)
+ return aLogins[i];
+ return null;
+ },
+
+ savePassword: function pu_savePassword(aLogins, aUser, aPass, aHostname, aRealm) {
+ let selectedLogin = this.findLogin(aLogins, "username", aUser.value);
+
+ // If we didn't find an existing login, or if the username
+ // changed, save as a new login.
+ if (!selectedLogin) {
+ // add as new
+ var newLogin = Cc["@mozilla.org/login-manager/loginInfo;1"].createInstance(Ci.nsILoginInfo);
+ newLogin.init(aHostname, null, aRealm, aUser.value, aPass.value, "", "");
+ this.pwmgr.addLogin(newLogin);
+ } else if (aPass.value != selectedLogin.password) {
+ // update password
+ this.updateLogin(selectedLogin, aPass.value);
+ } else {
+ this.updateLogin(selectedLogin);
+ }
+ },
+
+ updateLogin: function pu_updateLogin(aLogin, aPassword) {
+ let now = Date.now();
+ let propBag = Cc["@mozilla.org/hash-property-bag;1"].createInstance(Ci.nsIWritablePropertyBag);
+ if (aPassword) {
+ propBag.setProperty("password", aPassword);
+ // Explicitly set the password change time here (even though it would
+ // be changed automatically), to ensure that it's exactly the same
+ // value as timeLastUsed.
+ propBag.setProperty("timePasswordChanged", now);
+ }
+ propBag.setProperty("timeLastUsed", now);
+ propBag.setProperty("timesUsedIncrement", 1);
+
+ this.pwmgr.modifyLogin(aLogin, propBag);
+ },
+
+ // JS port of http://mxr.mozilla.org/mozilla-central/source/embedding/components/windowwatcher/src/nsPrompt.cpp#388
+ makeDialogText: function pu_makeDialogText(aChannel, aAuthInfo) {
+ let isProxy = (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY);
+ let isPassOnly = (aAuthInfo.flags & Ci.nsIAuthInformation.ONLY_PASSWORD);
+
+ let username = aAuthInfo.username;
+ let [displayHost, realm] = this.getAuthTarget(aChannel, aAuthInfo);
+
+ // Suppress "the site says: $realm" when we synthesized a missing realm.
+ if (!aAuthInfo.realm && !isProxy)
+ realm = "";
+
+ // Trim obnoxiously long realms.
+ if (realm.length > 150) {
+ realm = realm.substring(0, 150);
+ // Append "..." (or localized equivalent).
+ realm += this.ellipsis;
+ }
+
+ let text;
+ if (isProxy)
+ text = this.bundle.formatStringFromName("EnterLoginForProxy", [realm, displayHost], 2);
+ else if (isPassOnly)
+ text = this.bundle.formatStringFromName("EnterPasswordFor", [username, displayHost], 2);
+ else if (!realm)
+ text = this.bundle.formatStringFromName("EnterUserPasswordFor", [displayHost], 1);
+ else
+ text = this.bundle.formatStringFromName("EnterLoginForRealm", [realm, displayHost], 2);
+
+ return text;
+ },
+
+ // JS port of http://mxr.mozilla.org/mozilla-central/source/embedding/components/windowwatcher/public/nsPromptUtils.h#89
+ getAuthHostPort: function pu_getAuthHostPort(aChannel, aAuthInfo) {
+ let uri = aChannel.URI;
+ let res = { host: null, port: -1 };
+ if (aAuthInfo.flags & aAuthInfo.AUTH_PROXY) {
+ let proxy = aChannel.QueryInterface(Ci.nsIProxiedChannel);
+ res.host = proxy.proxyInfo.host;
+ res.port = proxy.proxyInfo.port;
+ } else {
+ res.host = uri.host;
+ res.port = uri.port;
+ }
+ return res;
+ },
+
+ getAuthTarget : function pu_getAuthTarget(aChannel, aAuthInfo) {
+ let hostname, realm;
+ // If our proxy is demanding authentication, don't use the
+ // channel's actual destination.
+ if (aAuthInfo.flags & Ci.nsIAuthInformation.AUTH_PROXY) {
+ if (!(aChannel instanceof Ci.nsIProxiedChannel))
+ throw "proxy auth needs nsIProxiedChannel";
+
+ let info = aChannel.proxyInfo;
+ if (!info)
+ throw "proxy auth needs nsIProxyInfo";
+
+ // Proxies don't have a scheme, but we'll use "moz-proxy://"
+ // so that it's more obvious what the login is for.
+ let idnService = Cc["@mozilla.org/network/idn-service;1"].getService(Ci.nsIIDNService);
+ hostname = "moz-proxy://" + idnService.convertUTF8toACE(info.host) + ":" + info.port;
+ realm = aAuthInfo.realm;
+ if (!realm)
+ realm = hostname;
+
+ return [hostname, realm];
+ }
+ hostname = this.getFormattedHostname(aChannel.URI);
+
+ // If a HTTP WWW-Authenticate header specified a realm, that value
+ // will be available here. If it wasn't set or wasn't HTTP, we'll use
+ // the formatted hostname instead.
+ realm = aAuthInfo.realm;
+ if (!realm)
+ realm = hostname;
+
+ return [hostname, realm];
+ },
+
+ getAuthInfo : function pu_getAuthInfo(aAuthInfo) {
+ let flags = aAuthInfo.flags;
+ let username = {value: ""};
+ let password = {value: ""};
+
+ if (flags & Ci.nsIAuthInformation.NEED_DOMAIN && aAuthInfo.domain)
+ username.value = aAuthInfo.domain + "\\" + aAuthInfo.username;
+ else
+ username.value = aAuthInfo.username;
+
+ password.value = aAuthInfo.password
+
+ return [username, password];
+ },
+
+ setAuthInfo : function (aAuthInfo, username, password) {
+ var flags = aAuthInfo.flags;
+ if (flags & Ci.nsIAuthInformation.NEED_DOMAIN) {
+ // Domain is separated from username by a backslash
+ var idx = username.indexOf("\\");
+ if (idx == -1) {
+ aAuthInfo.username = username;
+ } else {
+ aAuthInfo.domain = username.substring(0, idx);
+ aAuthInfo.username = username.substring(idx+1);
+ }
+ } else {
+ aAuthInfo.username = username;
+ }
+ aAuthInfo.password = password;
+ },
+
+ getFormattedHostname : function pu_getFormattedHostname(uri) {
+ let scheme = uri.scheme;
+ let hostname = scheme + "://" + uri.host;
+
+ // If the URI explicitly specified a port, only include it when
+ // it's not the default. (We never want "http://foo.com:80")
+ port = uri.port;
+ if (port != -1) {
+ let handler = Services.io.getProtocolHandler(scheme);
+ if (port != handler.defaultPort)
+ hostname += ":" + port;
+ }
+ return hostname;
+ },
+};
+
+XPCOMUtils.defineLazyGetter(PromptUtils, "passwdBundle", function () {
+ return Services.strings.createBundle("chrome://passwordmgr/locale/passwordmgr.properties");
+});
+
+XPCOMUtils.defineLazyGetter(PromptUtils, "bundle", function () {
+ return Services.strings.createBundle("chrome://global/locale/commonDialogs.properties");
+});
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PromptService]);
diff --git a/browser/metro/components/SafeBrowsing.js b/browser/metro/components/SafeBrowsing.js
new file mode 100644
index 000000000000..d1d31bcf5d59
--- /dev/null
+++ b/browser/metro/components/SafeBrowsing.js
@@ -0,0 +1,168 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const kPhishWardenEnabledPref = "browser.safebrowsing.enabled";
+const kMalwareWardenEnabledPref = "browser.safebrowsing.malware.enabled";
+
+// This XPCOM object doesn't have a public interface. It just works quietly in the background
+function SafeBrowsing() {
+ this.listManager = null;
+
+ // Once we register tables, their respective names will be listed here.
+ this.phishing = {
+ pref: kPhishWardenEnabledPref,
+ blackTables: [],
+ whiteTables: []
+ };
+ this.malware = {
+ pref: kMalwareWardenEnabledPref,
+ blackTables: [],
+ whiteTables: []
+ };
+
+ // Get notifications when the phishing or malware warden enabled pref changes
+ Services.prefs.addObserver(kPhishWardenEnabledPref, this, true);
+ Services.prefs.addObserver(kMalwareWardenEnabledPref, this, true);
+}
+
+SafeBrowsing.prototype = {
+ classID: Components.ID("{aadaed90-6c03-42d0-924a-fc61198ff283}"),
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISessionStore,
+ Ci.nsIDOMEventListener,
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference]),
+
+ observe: function sb_observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "app-startup":
+ Services.obs.addObserver(this, "final-ui-startup", true);
+ Services.obs.addObserver(this, "xpcom-shutdown", true);
+ break;
+ case "final-ui-startup":
+ Services.obs.removeObserver(this, "final-ui-startup");
+ this._startup();
+ break;
+ case "xpcom-shutdown":
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+ this._shutdown();
+ break;
+ case "nsPref:changed":
+ if (aData == kPhishWardenEnabledPref)
+ this.maybeToggleUpdateChecking(this.phishing);
+ else if (aData == kMalwareWardenEnabledPref)
+ this.maybeToggleUpdateChecking(this.malware);
+ break;
+ }
+ },
+
+ _startup: function sb_startup() {
+ this.listManager = Cc["@mozilla.org/url-classifier/listmanager;1"].getService(Ci.nsIUrlListManager);
+
+ // Add a test chunk to the database
+ let testData = "mozilla.org/firefox/its-an-attack.html";
+ let testUpdate =
+ "n:1000\ni:test-malware-simple\nad:1\n" +
+ "a:1:32:" + testData.length + "\n" +
+ testData;
+
+ testData = "mozilla.org/firefox/its-a-trap.html";
+ testUpdate +=
+ "n:1000\ni:test-phish-simple\nad:1\n" +
+ "a:1:32:" + testData.length + "\n" +
+ testData;
+
+ let dbService = Cc["@mozilla.org/url-classifier/dbservice;1"].getService(Ci.nsIUrlClassifierDBService);
+
+ let listener = {
+ QueryInterface: function(aIID) {
+ if (aIID.equals(Ci.nsISupports) || aIID.equals(Ci.nsIUrlClassifierUpdateObserver))
+ return this;
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ },
+
+ updateUrlRequested: function(aURL) { },
+ streamFinished: function(aStatus) { },
+ updateError: function(aErrorCode) { },
+ updateSuccess: function(aRequestedTimeout) { }
+ };
+
+ try {
+ dbService.beginUpdate(listener, "test-malware-simple,test-phish-simple", "");
+ dbService.beginStream("", "");
+ dbService.updateStream(testUpdate);
+ dbService.finishStream();
+ dbService.finishUpdate();
+ } catch(ex) {}
+
+ this.registerBlackTable("goog-malware-shavar", this.malware);
+ this.maybeToggleUpdateChecking(this.malware);
+
+ this.registerBlackTable("goog-phish-shavar", this.phishing);
+ this.maybeToggleUpdateChecking(this.phishing);
+ },
+
+ _shutdown: function sb_shutdown() {
+ Services.prefs.removeObserver(kPhishWardenEnabledPref, this);
+ Services.prefs.removeObserver(kMalwareWardenEnabledPref, this);
+
+ this.listManager = null;
+ },
+
+ enableBlacklistTableUpdates: function sb_enableBlacklistTableUpdates(aWarden) {
+ for (let i = 0; i < aWarden.blackTables.length; ++i) {
+ this.listManager.enableUpdate(aWarden.blackTables[i]);
+ }
+ },
+
+ disableBlacklistTableUpdates: function sb_disableBlacklistTableUpdates(aWarden) {
+ for (let i = 0; i < aWarden.blackTables.length; ++i) {
+ this.listManager.disableUpdate(aWarden.blackTables[i]);
+ }
+ },
+
+ enableWhitelistTableUpdates: function sb_enableWhitelistTableUpdates(aWarden) {
+ for (let i = 0; i < this.whiteTables.length; ++i) {
+ this.listManager.enableUpdate(this.whiteTables[i]);
+ }
+ },
+
+ disableWhitelistTableUpdates: function sb_disableWhitelistTableUpdates(aWarden) {
+ for (let i = 0; i < aWarden.whiteTables.length; ++i) {
+ this.listManager.disableUpdate(aWarden.whiteTables[i]);
+ }
+ },
+
+ registerBlackTable: function sb_registerBlackTable(aTableName, aWarden) {
+ let result = this.listManager.registerTable(aTableName, false);
+ if (result)
+ aWarden.blackTables.push(aTableName);
+ return result;
+ },
+
+ registerWhiteTable: function sb_registerWhiteTable(aTableName, aWarden) {
+ let result = this.listManager.registerTable(aTableName, false);
+ if (result)
+ aWarden.whiteTables.push(aTableName);
+ return result;
+ },
+
+ maybeToggleUpdateChecking: function sb_maybeToggleUpdateChecking(aWarden) {
+ let enabled = Services.prefs.getBoolPref(aWarden.pref);
+ if (enabled)
+ this.enableBlacklistTableUpdates(aWarden);
+ else
+ this.disableBlacklistTableUpdates(aWarden);
+ }
+}
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([SafeBrowsing]);
diff --git a/browser/metro/components/SessionStore.idl b/browser/metro/components/SessionStore.idl
new file mode 100644
index 000000000000..a5bf20fafaf4
--- /dev/null
+++ b/browser/metro/components/SessionStore.idl
@@ -0,0 +1,83 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIDOMWindow;
+interface nsIDOMNode;
+
+/**
+ * nsISessionStore keeps track of the current browsing state.
+ *
+ * The nsISessionStore API operates mostly on browser windows and the browser
+ * tabs contained in them.
+ */
+
+[scriptable, uuid(766a09c1-d21b-4bf8-9fe3-8b34b716251a)]
+interface nsISessionStore : nsISupports
+{
+ /**
+ * Get the current browsing state.
+ * @returns a JSON string representing the session state.
+ */
+ AString getBrowserState();
+
+ /**
+ * Get the number of restore-able tabs for a browser window
+ */
+ unsigned long getClosedTabCount(in nsIDOMWindow aWindow);
+
+ /**
+ * Get closed tab data
+ *
+ * @param aWindow is the browser window for which to get closed tab data
+ * @returns a JSON string representing the list of closed tabs.
+ */
+ AString getClosedTabData(in nsIDOMWindow aWindow);
+
+ /**
+ * @param aWindow is the browser window to reopen a closed tab in.
+ * @param aIndex is the index of the tab to be restored (FIFO ordered).
+ * @returns a reference to the reopened tab.
+ */
+ nsIDOMNode undoCloseTab(in nsIDOMWindow aWindow, in unsigned long aIndex);
+
+ /**
+ * @param aWindow is the browser window associated with the closed tab.
+ * @param aIndex is the index of the closed tab to be removed (FIFO ordered).
+ */
+ nsIDOMNode forgetClosedTab(in nsIDOMWindow aWindow, in unsigned long aIndex);
+
+ /**
+ * @param aTab is the browser tab to get the value for.
+ * @param aKey is the value's name.
+ *
+ * @returns A string value or an empty string if none is set.
+ */
+ AString getTabValue(in nsIDOMNode aTab, in AString aKey);
+
+ /**
+ * @param aTab is the browser tab to set the value for.
+ * @param aKey is the value's name.
+ * @param aStringValue is the value itself (use JSON.stringify/parse before setting JS objects).
+ */
+ void setTabValue(in nsIDOMNode aTab, in AString aKey, in AString aStringValue);
+
+ /**
+ * @param aTab is the browser tab to get the value for.
+ * @param aKey is the value's name.
+ */
+ void deleteTabValue(in nsIDOMNode aTab, in AString aKey);
+
+ /**
+ * @returns A boolean indicating we should restore previous browser session
+ */
+ boolean shouldRestore();
+
+ /**
+ * Restores the previous browser session using a fast, lightweight strategy
+ * @param aBringToFront should a restored tab be brought to the foreground?
+ */
+ void restoreLastSession(in boolean aBringToFront);
+};
diff --git a/browser/metro/components/SessionStore.js b/browser/metro/components/SessionStore.js
new file mode 100644
index 000000000000..71dedcb2d3e2
--- /dev/null
+++ b/browser/metro/components/SessionStore.js
@@ -0,0 +1,768 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+#ifdef MOZ_CRASHREPORTER
+XPCOMUtils.defineLazyServiceGetter(this, "CrashReporter",
+ "@mozilla.org/xre/app-info;1", "nsICrashReporter");
+#endif
+
+XPCOMUtils.defineLazyGetter(this, "NetUtil", function() {
+ Cu.import("resource://gre/modules/NetUtil.jsm");
+ return NetUtil;
+});
+
+// -----------------------------------------------------------------------
+// Session Store
+// -----------------------------------------------------------------------
+
+const STATE_STOPPED = 0;
+const STATE_RUNNING = 1;
+const STATE_QUITTING = -1;
+
+function SessionStore() { }
+
+SessionStore.prototype = {
+ classID: Components.ID("{8c1f07d6-cba3-4226-a315-8bd43d67d032}"),
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISessionStore,
+ Ci.nsIDOMEventListener,
+ Ci.nsIObserver,
+ Ci.nsISupportsWeakReference]),
+
+ _windows: {},
+ _lastSaveTime: 0,
+ _lastSessionTime: 0,
+ _interval: 10000,
+ _maxTabsUndo: 1,
+ _shouldRestore: false,
+
+ init: function ss_init() {
+ // Get file references
+ this._sessionFile = Services.dirsvc.get("ProfD", Ci.nsILocalFile);
+ this._sessionFileBackup = this._sessionFile.clone();
+ this._sessionCache = this._sessionFile.clone();
+ this._sessionFile.append("sessionstore.js");
+ this._sessionFileBackup.append("sessionstore.bak");
+ this._sessionCache.append("sessionstoreCache");
+
+ this._loadState = STATE_STOPPED;
+
+ try {
+ if (this._sessionFileBackup.exists()) {
+ this._shouldRestore = true;
+ this._sessionFileBackup.remove(false);
+ }
+
+ if (this._sessionFile.exists()) {
+ // Disable crash recovery if we have exceeded the timeout
+ this._lastSessionTime = this._sessionFile.lastModifiedTime;
+ let delta = Date.now() - this._lastSessionTime;
+ let timeout = Services.prefs.getIntPref("browser.sessionstore.resume_from_crash_timeout");
+ if (delta > (timeout * 60000))
+ this._shouldRestore = false;
+
+ this._sessionFile.copyTo(null, this._sessionFileBackup.leafName);
+ }
+
+ if (!this._sessionCache.exists() || !this._sessionCache.isDirectory())
+ this._sessionCache.create(Ci.nsIFile.DIRECTORY_TYPE, 0700);
+ } catch (ex) {
+ Cu.reportError(ex); // file was write-locked?
+ }
+
+ this._interval = Services.prefs.getIntPref("browser.sessionstore.interval");
+ this._maxTabsUndo = Services.prefs.getIntPref("browser.sessionstore.max_tabs_undo");
+
+ // Disable crash recovery if it has been turned off
+ if (!Services.prefs.getBoolPref("browser.sessionstore.resume_from_crash"))
+ this._shouldRestore = false;
+
+ // Do we need to restore session just this once, in case of a restart?
+ if (Services.prefs.getBoolPref("browser.sessionstore.resume_session_once")) {
+ Services.prefs.setBoolPref("browser.sessionstore.resume_session_once", false);
+ this._shouldRestore = true;
+ }
+ },
+
+ _clearDisk: function ss_clearDisk() {
+ if (this._sessionFile.exists()) {
+ try {
+ this._sessionFile.remove(false);
+ } catch (ex) { dump(ex + '\n'); } // couldn't remove the file - what now?
+ }
+ if (this._sessionFileBackup.exists()) {
+ try {
+ this._sessionFileBackup.remove(false);
+ } catch (ex) { dump(ex + '\n'); } // couldn't remove the file - what now?
+ }
+
+ this._clearCache();
+ },
+
+ _clearCache: function ss_clearCache() {
+ // First, let's get a list of files we think should be active
+ let activeFiles = [];
+ this._forEachBrowserWindow(function(aWindow) {
+ let tabs = aWindow.Browser.tabs;
+ for (let i = 0; i < tabs.length; i++) {
+ let browser = tabs[i].browser;
+ if (browser.__SS_extdata && "thumbnail" in browser.__SS_extdata)
+ activeFiles.push(browser.__SS_extdata.thumbnail);
+ }
+ });
+
+ // Now, let's find the stale files in the cache folder
+ let staleFiles = [];
+ let cacheFiles = this._sessionCache.directoryEntries;
+ while (cacheFiles.hasMoreElements()) {
+ let file = cacheFiles.getNext().QueryInterface(Ci.nsILocalFile);
+ let fileURI = Services.io.newFileURI(file);
+ if (activeFiles.indexOf(fileURI) == -1)
+ staleFiles.push(file);
+ }
+
+ // Remove the stale files in a separate step to keep the enumerator from
+ // messing up if we remove the files as we collect them.
+ staleFiles.forEach(function(aFile) {
+ aFile.remove(false);
+ })
+ },
+
+ observe: function ss_observe(aSubject, aTopic, aData) {
+ let self = this;
+ let observerService = Services.obs;
+ switch (aTopic) {
+ case "app-startup":
+ observerService.addObserver(this, "final-ui-startup", true);
+ observerService.addObserver(this, "domwindowopened", true);
+ observerService.addObserver(this, "domwindowclosed", true);
+ observerService.addObserver(this, "browser-lastwindow-close-granted", true);
+ observerService.addObserver(this, "browser:purge-session-history", true);
+ observerService.addObserver(this, "quit-application-requested", true);
+ observerService.addObserver(this, "quit-application-granted", true);
+ observerService.addObserver(this, "quit-application", true);
+ break;
+ case "final-ui-startup":
+ observerService.removeObserver(this, "final-ui-startup");
+ this.init();
+ break;
+ case "domwindowopened":
+ let window = aSubject;
+ window.addEventListener("load", function() {
+ self.onWindowOpen(window);
+ window.removeEventListener("load", arguments.callee, false);
+ }, false);
+ break;
+ case "domwindowclosed": // catch closed windows
+ this.onWindowClose(aSubject);
+ break;
+ case "browser-lastwindow-close-granted":
+ // If a save has been queued, kill the timer and save state now
+ if (this._saveTimer) {
+ this._saveTimer.cancel();
+ this._saveTimer = null;
+ this.saveState();
+ }
+
+ // Freeze the data at what we've got (ignoring closing windows)
+ this._loadState = STATE_QUITTING;
+ break;
+ case "quit-application-requested":
+ // Get a current snapshot of all windows
+ this._forEachBrowserWindow(function(aWindow) {
+ self._collectWindowData(aWindow);
+ });
+ break;
+ case "quit-application-granted":
+ // Get a current snapshot of all windows
+ this._forEachBrowserWindow(function(aWindow) {
+ self._collectWindowData(aWindow);
+ });
+
+ // Freeze the data at what we've got (ignoring closing windows)
+ this._loadState = STATE_QUITTING;
+ break;
+ case "quit-application":
+ // If we are restarting, lets restore the tabs
+ if (aData == "restart") {
+ Services.prefs.setBoolPref("browser.sessionstore.resume_session_once", true);
+
+ // Ignore purges when restarting. The notification is fired after "quit-application".
+ Services.obs.removeObserver(this, "browser:purge-session-history");
+ }
+
+ // Freeze the data at what we've got (ignoring closing windows)
+ this._loadState = STATE_QUITTING;
+
+ // No need for this back up, we are shutting down just fine
+ if (this._sessionFileBackup.exists())
+ this._sessionFileBackup.remove(false);
+
+ observerService.removeObserver(this, "domwindowopened");
+ observerService.removeObserver(this, "domwindowclosed");
+ observerService.removeObserver(this, "browser-lastwindow-close-granted");
+ observerService.removeObserver(this, "quit-application-requested");
+ observerService.removeObserver(this, "quit-application-granted");
+ observerService.removeObserver(this, "quit-application");
+
+ // If a save has been queued, kill the timer and save state now
+ if (this._saveTimer) {
+ this._saveTimer.cancel();
+ this._saveTimer = null;
+ this.saveState();
+ }
+ break;
+ case "browser:purge-session-history": // catch sanitization
+ this._clearDisk();
+
+ // If the browser is shutting down, simply return after clearing the
+ // session data on disk as this notification fires after the
+ // quit-application notification so the browser is about to exit.
+ if (this._loadState == STATE_QUITTING)
+ return;
+
+ // Clear all data about closed tabs
+ for (let [ssid, win] in Iterator(this._windows))
+ win.closedTabs = [];
+
+ if (this._loadState == STATE_RUNNING) {
+ // Save the purged state immediately
+ this.saveStateNow();
+ }
+ break;
+ case "timer-callback":
+ // Timer call back for delayed saving
+ this._saveTimer = null;
+ this.saveState();
+ break;
+ }
+ },
+
+ handleEvent: function ss_handleEvent(aEvent) {
+ let window = aEvent.currentTarget.ownerDocument.defaultView;
+ switch (aEvent.type) {
+ case "TabOpen":
+ case "TabClose": {
+ let browser = aEvent.originalTarget.linkedBrowser;
+ if (aEvent.type == "TabOpen") {
+ this.onTabAdd(window, browser);
+ }
+ else {
+ this.onTabClose(window, browser);
+ this.onTabRemove(window, browser);
+ }
+ break;
+ }
+ case "TabSelect": {
+ let browser = aEvent.originalTarget.linkedBrowser;
+ this.onTabSelect(window, browser);
+ break;
+ }
+ }
+ },
+
+ receiveMessage: function ss_receiveMessage(aMessage) {
+ let window = aMessage.target.ownerDocument.defaultView;
+ this.onTabLoad(window, aMessage.target, aMessage);
+ },
+
+ onWindowOpen: function ss_onWindowOpen(aWindow) {
+ // Return if window has already been initialized
+ if (aWindow && aWindow.__SSID && this._windows[aWindow.__SSID])
+ return;
+
+ // Ignore non-browser windows and windows opened while shutting down
+ if (aWindow.document.documentElement.getAttribute("windowtype") != "navigator:browser" || this._loadState == STATE_QUITTING)
+ return;
+
+ // Assign it a unique identifier (timestamp) and create its data object
+ aWindow.__SSID = "window" + Date.now();
+ this._windows[aWindow.__SSID] = { tabs: [], selected: 0, closedTabs: [] };
+
+ // Perform additional initialization when the first window is loading
+ if (this._loadState == STATE_STOPPED) {
+ this._loadState = STATE_RUNNING;
+ this._lastSaveTime = Date.now();
+
+ // Nothing to restore, notify observers things are complete
+ if (!this._shouldRestore) {
+ this._clearCache();
+ Services.obs.notifyObservers(null, "sessionstore-windows-restored", "");
+ }
+ }
+
+ // Add tab change listeners to all already existing tabs
+ let tabs = aWindow.Browser.tabs;
+ for (let i = 0; i < tabs.length; i++)
+ this.onTabAdd(aWindow, tabs[i].browser, true);
+
+ // Notification of tab add/remove/selection
+ let tabContainer = aWindow.document.getElementById("tabs");
+ tabContainer.addEventListener("TabOpen", this, true);
+ tabContainer.addEventListener("TabClose", this, true);
+ tabContainer.addEventListener("TabSelect", this, true);
+ },
+
+ onWindowClose: function ss_onWindowClose(aWindow) {
+ // Ignore windows not tracked by SessionStore
+ if (!aWindow.__SSID || !this._windows[aWindow.__SSID])
+ return;
+
+ let tabContainer = aWindow.document.getElementById("tabs");
+ tabContainer.removeEventListener("TabOpen", this, true);
+ tabContainer.removeEventListener("TabClose", this, true);
+ tabContainer.removeEventListener("TabSelect", this, true);
+
+ if (this._loadState == STATE_RUNNING) {
+ // Update all window data for a last time
+ this._collectWindowData(aWindow);
+
+ // Clear this window from the list
+ delete this._windows[aWindow.__SSID];
+
+ // Save the state without this window to disk
+ this.saveStateDelayed();
+ }
+
+ let tabs = aWindow.Browser.tabs;
+ for (let i = 0; i < tabs.length; i++)
+ this.onTabRemove(aWindow, tabs[i].browser, true);
+
+ delete aWindow.__SSID;
+ },
+
+ onTabAdd: function ss_onTabAdd(aWindow, aBrowser, aNoNotification) {
+ aBrowser.messageManager.addMessageListener("pageshow", this);
+ aBrowser.messageManager.addMessageListener("Content:SessionHistory", this);
+
+ if (!aNoNotification)
+ this.saveStateDelayed();
+ this._updateCrashReportURL(aWindow);
+ },
+
+ onTabRemove: function ss_onTabRemove(aWindow, aBrowser, aNoNotification) {
+ aBrowser.messageManager.removeMessageListener("pageshow", this);
+ aBrowser.messageManager.removeMessageListener("Content:SessionHistory", this);
+
+ // If this browser is being restored, skip any session save activity
+ if (aBrowser.__SS_restore)
+ return;
+
+ delete aBrowser.__SS_data;
+
+ if (!aNoNotification)
+ this.saveStateDelayed();
+ },
+
+ onTabClose: function ss_onTabClose(aWindow, aBrowser) {
+ if (this._maxTabsUndo == 0)
+ return;
+
+ if (aWindow.Browser.tabs.length > 0) {
+ // Bundle this browser's data and extra data and save in the closedTabs
+ // window property
+ let data = aBrowser.__SS_data;
+ data.extData = aBrowser.__SS_extdata;
+
+ this._windows[aWindow.__SSID].closedTabs.unshift(data);
+ let length = this._windows[aWindow.__SSID].closedTabs.length;
+ if (length > this._maxTabsUndo)
+ this._windows[aWindow.__SSID].closedTabs.splice(this._maxTabsUndo, length - this._maxTabsUndo);
+ }
+ },
+
+ onTabLoad: function ss_onTabLoad(aWindow, aBrowser, aMessage) {
+ // If this browser is being restored, skip any session save activity
+ if (aBrowser.__SS_restore)
+ return;
+
+ // Ignore a transient "about:blank"
+ if (!aBrowser.canGoBack && aBrowser.currentURI.spec == "about:blank")
+ return;
+
+ if (aMessage.name == "Content:SessionHistory") {
+ delete aBrowser.__SS_data;
+ this._collectTabData(aBrowser, aMessage.json);
+ }
+
+ // Save out the state as quickly as possible
+ if (aMessage.name == "pageshow")
+ this.saveStateNow();
+
+ this._updateCrashReportURL(aWindow);
+ },
+
+ onTabSelect: function ss_onTabSelect(aWindow, aBrowser) {
+ if (this._loadState != STATE_RUNNING)
+ return;
+
+ let index = aWindow.Elements.browsers.selectedIndex;
+ this._windows[aWindow.__SSID].selected = parseInt(index) + 1; // 1-based
+
+ // Restore the resurrected browser
+ if (aBrowser.__SS_restore) {
+ let data = aBrowser.__SS_data;
+ if (data.entries.length > 0) {
+ let json = {
+ uri: data.entries[data.index - 1].url,
+ flags: null,
+ entries: data.entries,
+ index: data.index
+ };
+ aBrowser.messageManager.sendAsyncMessage("WebNavigation:LoadURI", json);
+ }
+
+ delete aBrowser.__SS_restore;
+ }
+
+ this._updateCrashReportURL(aWindow);
+ },
+
+ saveStateDelayed: function ss_saveStateDelayed() {
+ if (!this._saveTimer) {
+ // Interval until the next disk operation is allowed
+ let minimalDelay = this._lastSaveTime + this._interval - Date.now();
+
+ // If we have to wait, set a timer, otherwise saveState directly
+ let delay = Math.max(minimalDelay, 2000);
+ if (delay > 0) {
+ this._saveTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._saveTimer.init(this, delay, Ci.nsITimer.TYPE_ONE_SHOT);
+ } else {
+ this.saveState();
+ }
+ }
+ },
+
+ saveStateNow: function ss_saveStateNow() {
+ // Kill any queued timer and save immediately
+ if (this._saveTimer) {
+ this._saveTimer.cancel();
+ this._saveTimer = null;
+ }
+ this.saveState();
+ },
+
+ saveState: function ss_saveState() {
+ let data = this._getCurrentState();
+ this._writeFile(this._sessionFile, JSON.stringify(data));
+
+ this._lastSaveTime = Date.now();
+ },
+
+ _getCurrentState: function ss_getCurrentState() {
+ let self = this;
+ this._forEachBrowserWindow(function(aWindow) {
+ self._collectWindowData(aWindow);
+ });
+
+ let data = { windows: [] };
+ let index;
+ for (index in this._windows)
+ data.windows.push(this._windows[index]);
+ return data;
+ },
+
+ _collectTabData: function ss__collectTabData(aBrowser, aHistory) {
+ // If this browser is being restored, skip any session save activity
+ if (aBrowser.__SS_restore)
+ return;
+
+ let aHistory = aHistory || { entries: [{ url: aBrowser.currentURI.spec, title: aBrowser.contentTitle }], index: 1 };
+
+ let tabData = {};
+ tabData.entries = aHistory.entries;
+ tabData.index = aHistory.index;
+ tabData.attributes = { image: aBrowser.mIconURL };
+
+ aBrowser.__SS_data = tabData;
+ },
+
+ _collectWindowData: function ss__collectWindowData(aWindow) {
+ // Ignore windows not tracked by SessionStore
+ if (!aWindow.__SSID || !this._windows[aWindow.__SSID])
+ return;
+
+ let winData = this._windows[aWindow.__SSID];
+ winData.tabs = [];
+
+ let index = aWindow.Elements.browsers.selectedIndex;
+ winData.selected = parseInt(index) + 1; // 1-based
+
+ let tabs = aWindow.Browser.tabs;
+ for (let i = 0; i < tabs.length; i++) {
+ let browser = tabs[i].browser;
+ if (browser.__SS_data) {
+ let tabData = browser.__SS_data;
+ if (browser.__SS_extdata)
+ tabData.extData = browser.__SS_extdata;
+ winData.tabs.push(tabData);
+ }
+ }
+ },
+
+ _forEachBrowserWindow: function ss_forEachBrowserWindow(aFunc) {
+ let windowsEnum = Services.wm.getEnumerator("navigator:browser");
+ while (windowsEnum.hasMoreElements()) {
+ let window = windowsEnum.getNext();
+ if (window.__SSID && !window.closed)
+ aFunc.call(this, window);
+ }
+ },
+
+ _writeFile: function ss_writeFile(aFile, aData) {
+ let stateString = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
+ stateString.data = aData;
+ Services.obs.notifyObservers(stateString, "sessionstore-state-write", "");
+
+ // Don't touch the file if an observer has deleted all state data
+ if (!stateString.data)
+ return;
+
+ // Initialize the file output stream.
+ let ostream = Cc["@mozilla.org/network/safe-file-output-stream;1"].createInstance(Ci.nsIFileOutputStream);
+ ostream.init(aFile, 0x02 | 0x08 | 0x20, 0600, ostream.DEFER_OPEN);
+
+ // Obtain a converter to convert our data to a UTF-8 encoded input stream.
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+
+ // Asynchronously copy the data to the file.
+ let istream = converter.convertToInputStream(aData);
+ NetUtil.asyncCopy(istream, ostream, function(rc) {
+ if (Components.isSuccessCode(rc)) {
+ Services.obs.notifyObservers(null, "sessionstore-state-write-complete", "");
+ }
+ });
+ },
+
+ _updateCrashReportURL: function ss_updateCrashReportURL(aWindow) {
+#ifdef MOZ_CRASHREPORTER
+ try {
+ let currentURI = aWindow.Browser.selectedBrowser.currentURI.clone();
+ // if the current URI contains a username/password, remove it
+ try {
+ currentURI.userPass = "";
+ }
+ catch (ex) { } // ignore failures on about: URIs
+
+ CrashReporter.annotateCrashReport("URL", currentURI.spec);
+ }
+ catch (ex) {
+ // don't make noise when crashreporter is built but not enabled
+ if (ex.result != Components.results.NS_ERROR_NOT_INITIALIZED)
+ Components.utils.reportError("SessionStore:" + ex);
+ }
+#endif
+ },
+
+ getBrowserState: function ss_getBrowserState() {
+ let data = this._getCurrentState();
+ return JSON.stringify(data);
+ },
+
+ getClosedTabCount: function ss_getClosedTabCount(aWindow) {
+ if (!aWindow || !aWindow.__SSID)
+ return 0; // not a browser window, or not otherwise tracked by SS.
+
+ return this._windows[aWindow.__SSID].closedTabs.length;
+ },
+
+ getClosedTabData: function ss_getClosedTabData(aWindow) {
+ if (!aWindow.__SSID)
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+
+ return JSON.stringify(this._windows[aWindow.__SSID].closedTabs);
+ },
+
+ undoCloseTab: function ss_undoCloseTab(aWindow, aIndex) {
+ if (!aWindow.__SSID)
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+
+ let closedTabs = this._windows[aWindow.__SSID].closedTabs;
+ if (!closedTabs)
+ return null;
+
+ // default to the most-recently closed tab
+ aIndex = aIndex || 0;
+ if (!(aIndex in closedTabs))
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+
+ // fetch the data of closed tab, while removing it from the array
+ let closedTab = closedTabs.splice(aIndex, 1).shift();
+
+ // create a new tab and bring to front
+ let tab = aWindow.Browser.addTab(closedTab.entries[closedTab.index - 1].url, true);
+
+ tab.browser.messageManager.sendAsyncMessage("WebNavigation:LoadURI", {
+ uri: closedTab.entries[closedTab.index - 1].url,
+ flags: null,
+ entries: closedTab.entries,
+ index: closedTab.index
+ });
+
+ // Put back the extra data
+ tab.browser.__SS_extdata = closedTab.extData;
+
+ return tab.chromeTab;
+ },
+
+ forgetClosedTab: function ss_forgetClosedTab(aWindow, aIndex) {
+ if (!aWindow.__SSID)
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+
+ let closedTabs = this._windows[aWindow.__SSID].closedTabs;
+
+ // default to the most-recently closed tab
+ aIndex = aIndex || 0;
+ if (!(aIndex in closedTabs))
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+
+ // remove closed tab from the array
+ closedTabs.splice(aIndex, 1);
+ },
+
+ getTabValue: function ss_getTabValue(aTab, aKey) {
+ let browser = aTab.linkedBrowser;
+ let data = browser.__SS_extdata || {};
+ return data[aKey] || "";
+ },
+
+ setTabValue: function ss_setTabValue(aTab, aKey, aStringValue) {
+ let browser = aTab.linkedBrowser;
+
+ // Thumbnails are actually stored in the cache, so do the save and update the URI
+ if (aKey == "thumbnail") {
+ let file = this._sessionCache.clone();
+ file.append("thumbnail-" + browser.contentWindowId);
+ file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0600);
+
+ let source = Services.io.newURI(aStringValue, "UTF8", null);
+ let target = Services.io.newFileURI(file)
+
+ let persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"].createInstance(Ci.nsIWebBrowserPersist);
+ persist.persistFlags = Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES | Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION;
+ persist.saveURI(source, null, null, null, null, file);
+
+ aStringValue = target.spec;
+ }
+
+ if (!browser.__SS_extdata)
+ browser.__SS_extdata = {};
+ browser.__SS_extdata[aKey] = aStringValue;
+ this.saveStateDelayed();
+ },
+
+ deleteTabValue: function ss_deleteTabValue(aTab, aKey) {
+ let browser = aTab.linkedBrowser;
+ if (browser.__SS_extdata && browser.__SS_extdata[aKey])
+ delete browser.__SS_extdata[aKey];
+ else
+ throw (Components.returnCode = Cr.NS_ERROR_INVALID_ARG);
+ },
+
+ shouldRestore: function ss_shouldRestore() {
+ return this._shouldRestore;
+ },
+
+ restoreLastSession: function ss_restoreLastSession(aBringToFront) {
+ let self = this;
+ function notifyObservers(aMessage) {
+ self._clearCache();
+ Services.obs.notifyObservers(null, "sessionstore-windows-restored", aMessage || "");
+ }
+
+ // The previous session data has already been renamed to the backup file
+ if (!this._sessionFileBackup.exists()) {
+ notifyObservers("fail")
+ return;
+ }
+
+ try {
+ let channel = NetUtil.newChannel(this._sessionFileBackup);
+ channel.contentType = "application/json";
+ NetUtil.asyncFetch(channel, function(aStream, aResult) {
+ if (!Components.isSuccessCode(aResult)) {
+ Cu.reportError("SessionStore: Could not read from sessionstore.bak file");
+ notifyObservers("fail");
+ return;
+ }
+
+ // Read session state file into a string and let observers modify the state before it's being used
+ let state = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
+ state.data = NetUtil.readInputStreamToString(aStream, aStream.available(), { charset : "UTF-8" }) || "";
+ aStream.close();
+
+ Services.obs.notifyObservers(state, "sessionstore-state-read", "");
+
+ let data = null;
+ try {
+ data = JSON.parse(state.data);
+ } catch (ex) {
+ Cu.reportError("SessionStore: Could not parse JSON: " + ex);
+ }
+
+ if (!data || data.windows.length == 0) {
+ notifyObservers("fail");
+ return;
+ }
+
+ let window = Services.wm.getMostRecentWindow("navigator:browser");
+
+ let tabs = data.windows[0].tabs;
+ let selected = data.windows[0].selected;
+ if (selected > tabs.length) // Clamp the selected index if it's bogus
+ selected = 1;
+
+ for (let i=0; i 'nightly'
+BRANDFOLDER = $(notdir $(MOZ_BRANDING_DIRECTORY))
+
+branding-$(BRANDFOLDER):
+ $(MAKE) -C $(DEPTH)/browser/branding/$(BRANDFOLDER) \
+ DIST_SUBDIR=$(DIST_SUBDIR) XPI_ROOT_APPID="$(XPI_ROOT_APPID)"
+
+libs:: branding-$(BRANDFOLDER)
diff --git a/browser/metro/locales/en-US/chrome/aboutCertError.dtd b/browser/metro/locales/en-US/chrome/aboutCertError.dtd
new file mode 100644
index 000000000000..cd6c6ba632f4
--- /dev/null
+++ b/browser/metro/locales/en-US/chrome/aboutCertError.dtd
@@ -0,0 +1,38 @@
+
+
+
+ %brandDTD;
+
+
+
+
+
+
+
+#1, but we can't confirm that your connection is secure.">
+
+
+
+
+
+
+Even if you trust the site, this error could mean that someone is
+tampering with your connection.">
+
+
+
+
+
diff --git a/browser/metro/locales/en-US/chrome/browser.dtd b/browser/metro/locales/en-US/chrome/browser.dtd
new file mode 100644
index 000000000000..71dac3782f1d
--- /dev/null
+++ b/browser/metro/locales/en-US/chrome/browser.dtd
@@ -0,0 +1,107 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/metro/locales/en-US/chrome/browser.properties b/browser/metro/locales/en-US/chrome/browser.properties
new file mode 100644
index 000000000000..6d403c184fce
--- /dev/null
+++ b/browser/metro/locales/en-US/chrome/browser.properties
@@ -0,0 +1,140 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Default search engine
+browser.search.defaultenginename=Bing
+
+# Search engine order (order displayed in the search bar dropdown)s
+browser.search.order.1=Bing
+browser.search.order.2=Google
+browser.search.order.3=Yahoo
+
+# Settings Charms
+aboutCharm1=About
+optionsCharm=Options
+helpOnlineCharm=Help (online)
+
+# General
+browserForSaveLocation=Save Location
+
+# Download Manager
+downloadsUnknownSize=Unknown size
+
+# Alerts
+alertLinkBookmarked=Bookmark added
+alertDownloads=Downloads
+alertDownloadsStart=Downloading: %S
+alertDownloadsDone=%S has finished downloading
+alertCantOpenDownload=Can't open file. Tap to save it.
+alertDownloadsSize=Download too big
+alertDownloadsNoSpace=Not enough storage space
+
+# Popup Blocker
+popupWarning=%S prevented this site from opening a pop-up window.
+popupWarningMultiple=%S prevented this site from opening %S pop-up windows.
+popupButtonAllowOnce=Show
+popupButtonAlwaysAllow2=Always Show
+popupButtonNeverWarn2=Never Show
+
+# Site Identity
+identity.identified.verifier=Verified by: %S
+identity.identified.verified_by_you=You have added a security exception for this site
+identity.identified.state_and_country=%S, %S
+identity.identified.title_with_country=%S (%S)
+identity.encrypted2=Encrypted
+identity.unencrypted2=Not encrypted
+identity.unknown.tooltip=This website does not supply identity information.
+identity.ownerUnknown2=(unknown)
+
+# Geolocation UI
+geolocation.allow=Share
+geolocation.dontAllow=Don't share
+geolocation.wantsTo=%S wants your location.
+
+# Desktop notification UI
+desktopNotification.allow=Allow
+desktopNotification.dontAllow=Don't allow
+desktopNotification.wantsTo=%S wants to use notifications.
+
+# Error Console
+typeError=Error:
+typeWarning=Warning:
+
+# Offline web applications
+offlineApps.available2=%S wants to store data on your device for offline use.
+offlineApps.allow=Allow
+offlineApps.never=Don't Allow
+offlineApps.notNow=Not Now
+
+# New-style ContentPermissionPrompt values
+offlineApps.dontAllow=Don't Allow
+offlineApps.wantsTo=%S wants to store data on your device for offline use.
+
+# IndexedDB Quota increases
+indexedDBQuota.allow=Allow
+indexedDBQuota.dontAllow=Don't Allow
+indexedDBQuota.wantsTo=%S wants to store a lot of data on your device for offline use.
+
+# Open Web Apps management API
+openWebappsManage.allow=Allow
+openWebappsManage.dontAllow=Don't Allow
+openWebappsManage.wantsTo=%S wants to manage applications on your device.
+
+# Bookmark List
+bookmarkList.desktop=Desktop Bookmarks
+
+# Closing Tabs
+tabs.closeWarningTitle=Confirm close
+
+# LOCALIZATION NOTE (tabs.closeWarning): Semi-colon list of plural forms.
+# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
+# #1 number of tabs (must be greater than 1)
+tabs.closeWarning=NOT USED;You are about to close #1 tabs. Continue?
+
+tabs.closeButton=Close tabs
+tabs.closeWarningPromptMe=Warn me when I attempt to close multiple tabs
+tabs.emptyTabTitle=New Tab
+
+# Homepage
+# LOCALIZATION NOTE: homepage.custom2 is the text displayed on the selector button if
+# the user selects a webpage to be the startpage. We can't display the entire URL
+# or webpage title on the menulist
+homepage.custom2=Custom Page
+
+# Page Actions
+pageactions.pin.site=Pin Site
+pageactions.geolocation=Location
+pageactions.popup=Popups
+pageactions.offline-app=Offline Storage
+pageactions.password=Password
+pageactions.desktop-notification=Web Notifications
+pageactions.openWebappsManage=Manage Web Apps
+
+# Open Search
+opensearch.search=Search: %S
+
+# Open in Another App
+# LOCALIZATION NOTE: openinapp.specific is the text displayed if there is a single external app
+# %S is the name of the app, like "YouTube" or "Picassa"
+openinapp.specific=Open in %S App
+openinapp.general=Open in Another App
+
+# Clear Private Data
+clearPrivateData.title=Clear Private Data
+clearPrivateData.message=Delete your browsing history and settings, including passwords and cookies?
+
+# LOCALIZATION NOTE (browser.menu.showCharacterEncoding): Set to the string
+# "true" (spelled and capitalized exactly that way) to show the "Character
+# Encoding" menu in the site menu. Any other value will hide it. Without this
+# setting, the "Character Encoding" menu must be enabled via Preferences.
+# This is not a string to translate. If users frequently use the "Character Encoding"
+# menu, set this to "true". Otherwise, you can leave it as "false".
+browser.menu.showCharacterEncoding=false
+
+# LOCALIZATION NOTE (intl.charsetmenu.browser.static): Set to a series of comma separated
+# values for charsets that the user can select from in the Character Encoding menu.
+intl.charsetmenu.browser.static=iso-8859-1,utf-8,x-gbk,big5,iso-2022-jp,shift_jis,euc-jp
+
+#Text Selection
+selectionHelper.textCopied=Text copied to clipboard
diff --git a/browser/metro/locales/en-US/chrome/checkbox.dtd b/browser/metro/locales/en-US/chrome/checkbox.dtd
new file mode 100644
index 000000000000..523375f7c59f
--- /dev/null
+++ b/browser/metro/locales/en-US/chrome/checkbox.dtd
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/browser/metro/locales/en-US/chrome/config.dtd b/browser/metro/locales/en-US/chrome/config.dtd
new file mode 100644
index 000000000000..ac49b2d29bfb
--- /dev/null
+++ b/browser/metro/locales/en-US/chrome/config.dtd
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/metro/locales/en-US/chrome/notification.dtd b/browser/metro/locales/en-US/chrome/notification.dtd
new file mode 100644
index 000000000000..d45e71b67234
--- /dev/null
+++ b/browser/metro/locales/en-US/chrome/notification.dtd
@@ -0,0 +1,12 @@
+
+
+
+
+
diff --git a/browser/metro/locales/en-US/chrome/phishing.dtd b/browser/metro/locales/en-US/chrome/phishing.dtd
new file mode 100644
index 000000000000..b3fd118e12f5
--- /dev/null
+++ b/browser/metro/locales/en-US/chrome/phishing.dtd
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ has been reported as an attack page and has been blocked based on your security preferences.">
+Attack pages try to install programs that steal private information, use your computer to attack others, or damage your system.Some attack pages intentionally distribute harmful software, but many are compromised without the knowledge or permission of their owners.
">
+
+
+
+These types of web forgeries are used in scams known as phishing attacks, in which fraudulent web pages and emails are used to imitate sources you may trust.">
diff --git a/browser/metro/locales/en-US/chrome/preferences.dtd b/browser/metro/locales/en-US/chrome/preferences.dtd
new file mode 100644
index 000000000000..71c7834e7f41
--- /dev/null
+++ b/browser/metro/locales/en-US/chrome/preferences.dtd
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/metro/locales/en-US/chrome/prompt.dtd b/browser/metro/locales/en-US/chrome/prompt.dtd
new file mode 100644
index 000000000000..dd747db6aace
--- /dev/null
+++ b/browser/metro/locales/en-US/chrome/prompt.dtd
@@ -0,0 +1,6 @@
+
+
+
+
diff --git a/browser/metro/locales/en-US/chrome/sync.dtd b/browser/metro/locales/en-US/chrome/sync.dtd
new file mode 100644
index 000000000000..2d7e6e62f6cf
--- /dev/null
+++ b/browser/metro/locales/en-US/chrome/sync.dtd
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/metro/locales/en-US/chrome/sync.properties b/browser/metro/locales/en-US/chrome/sync.properties
new file mode 100644
index 000000000000..3cc69529fe9c
--- /dev/null
+++ b/browser/metro/locales/en-US/chrome/sync.properties
@@ -0,0 +1,31 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# Mobile Sync
+
+# %S is the date and time at which the last sync successfully completed
+lastSync2.label=Last sync: %S
+lastSyncInProgress2.label=Last sync: in progress…
+
+# %S is the username logged in
+account.label=Account: %S
+notconnected.label=Not connected
+connecting.label=Connecting…
+
+notificationDisconnect.label=Your Firefox Sync account has been removed
+notificationDisconnect.button=Undo
+
+# LOCALIZATION NOTE (sync.clientUpdate, sync.remoteUpdate):
+# #1 is the "application name"
+# #2 is the "version"
+sync.update.client=#1 #2 is not compatible with the latest version of Firefox Sync. Please update to the latest version.
+sync.update.remote=#1 #2 is not compatible with older versions of Firefox Sync. Please update Firefox on your other computer(s).
+sync.update.title=Firefox Sync
+sync.update.button=Learn More
+sync.update.close=Close
+sync.setup.error.title=Cannot Setup Sync
+sync.setup.error.network=No internet connection available
+sync.setup.error.nodata=%S could not connect to Sync. Would you like to try again?
+sync.setup.tryagain=Try again
+sync.setup.manual=Manual setup
diff --git a/browser/metro/locales/en-US/overrides/passwordmgr.properties b/browser/metro/locales/en-US/overrides/passwordmgr.properties
new file mode 100644
index 000000000000..933de16a7754
--- /dev/null
+++ b/browser/metro/locales/en-US/overrides/passwordmgr.properties
@@ -0,0 +1,40 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+rememberValue = Use Password Manager to remember this value.
+rememberPassword = Use Password Manager to remember this password.
+savePasswordTitle = Confirm
+# 1st string is product name, 2nd is the username for the login, 3rd is the
+# login's hostname. Note that long usernames may be truncated.
+saveLoginText = Do you want %1$S to remember the password for "%2$S" on %3$S?
+# 1st string is product name, 2nd is the login's hostname
+saveLoginTextNoUsername = Do you want %1$S to remember this password on %2$S?
+promptNotNowButtonText = Not Now
+notifyBarNotNowButtonText = Not Now
+notifyBarNotNowButtonAccessKey =
+promptNeverForSiteButtonText = Never
+notifyBarNeverForSiteButtonText = Never
+notifyBarNeverForSiteButtonAccessKey =
+promptRememberButtonText = Remember
+notifyBarRememberButtonText = Remember
+notifyBarRememberButtonAccessKey =
+passwordChangeTitle = Confirm Password Change
+passwordChangeText = Would you like to change the stored password for %S?
+passwordChangeTextNoUser = Would you like to change the stored password for this login?
+notifyBarChangeButtonText = Change
+notifyBarChangeButtonAccessKey =
+notifyBarDontChangeButtonText = Don't Change
+notifyBarDontChangeButtonAccessKey =
+userSelectText = Please confirm which user you are changing the password for
+hidePasswords=Hide Passwords
+hidePasswordsAccessKey=P
+showPasswords=Show Passwords
+showPasswordsAccessKey=P
+noMasterPasswordPrompt=Are you sure you wish to show your passwords?
+removeAllPasswordsPrompt=Are you sure you wish to remove all passwords?
+removeAllPasswordsTitle=Remove all passwords
+loginsSpielAll=Passwords for the following sites are stored on your computer:
+loginsSpielFiltered=The following passwords match your search:
+username=Username
+password=Password
diff --git a/browser/metro/locales/generic/profile/bookmarks.json.in b/browser/metro/locales/generic/profile/bookmarks.json.in
new file mode 100644
index 000000000000..95a5628e050a
--- /dev/null
+++ b/browser/metro/locales/generic/profile/bookmarks.json.in
@@ -0,0 +1,17 @@
+#filter substitution
+{"type":"text/x-moz-place-container","root":"placesRoot","children":
+ [{"type":"text/x-moz-place-container","title":"@bookmarks_title@","annos":[{"name":"metro/bookmarksRoot","expires":4,"type":1,"value":1}],
+ "children":
+ [
+ {"index":1,"title":"@firefox_about@", "type":"text/x-moz-place", "uri":"http://www.mozilla.com/@AB_CD@/about/",
+ "iconUri":"chrome://branding/content/favicon32.png"
+ },
+ {"index":2,"title":"@getting_started@", "type":"text/x-moz-place", "uri":"http://www.mozilla.com/@AB_CD@/firefox/central/",
+ "iconUri":"chrome://branding/content/favicon32.png"
+ },
+ {"index":3,"title":"@firefox_community@", "type":"text/x-moz-place", "uri":"http://www.mozilla.com/@AB_CD@/firefox/community/",
+ "iconUri":"chrome://branding/content/favicon32.png"
+ },
+ ]
+ }]
+}
diff --git a/browser/metro/locales/import/Makefile.in b/browser/metro/locales/import/Makefile.in
new file mode 100644
index 000000000000..76fc1bbbb156
--- /dev/null
+++ b/browser/metro/locales/import/Makefile.in
@@ -0,0 +1,56 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEPTH = ../../../..
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+# desktop specific resources - this defines LOCALE_SRCDIR
+relativesrcdir = browser/locales
+
+include $(DEPTH)/config/autoconf.mk
+include $(topsrcdir)/config/config.mk
+
+#########################################
+# l10s prefs file
+
+DEFINES += -DAB_CD=$(AB_CD)
+
+# copying firefox-l10n.js over from LOCALE_SRCDIR or browser
+PREF_JS_EXPORTS = $(firstword $(wildcard $(LOCALE_SRCDIR)/firefox-l10n.js) \
+ $(topsrcdir)/$(relativesrcdir)/en-US/firefox-l10n.js )
+
+include $(topsrcdir)/config/rules.mk
+
+#########################################
+# Search plugins
+
+# Metro reuses desktop search plugins
+libs::
+ $(MAKE) -C $(DEPTH)/browser/locales searchplugins \
+ DIST_SUBDIR=$(DIST_SUBDIR) XPI_ROOT_APPID="$(XPI_ROOT_APPID)"
+
+#########################################
+# Bookmarks
+
+# Pick up desktop's bookmarks.inc file
+vpath book%.inc $(LOCALE_SRCDIR)/profile
+ifdef LOCALE_MERGEDIR
+vpath book%.inc $(LOCALE_SRCDIR)/profile
+vpath book%.inc @top_srcdir@/$(relativesrcdir)/en-US/profile
+endif
+
+bookmarks-src = $(srcdir)/../generic/profile/bookmarks.json.in
+
+# The resulting bookmarks.json will get picked up and packaged by the
+# processing of the jar file in the parent directory.
+bookmarks: bookmarks.inc
+ @echo "Generating: $@"
+ $(PYTHON) $(topsrcdir)/config/Preprocessor.py \
+ -I $^ \
+ -DAB_CD=$(AB_CD) \
+ $(bookmarks-src) > ../bookmarks.json
+
+libs:: bookmarks
diff --git a/browser/metro/locales/jar.mn b/browser/metro/locales/jar.mn
new file mode 100644
index 000000000000..4a3415db9db4
--- /dev/null
+++ b/browser/metro/locales/jar.mn
@@ -0,0 +1,40 @@
+#filter substitution
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+#
+# Metro jar resources
+#
+
+@AB_CD@.jar:
+% locale browser @AB_CD@ %locale/browser/
+ locale/browser/aboutCertError.dtd (%chrome/aboutCertError.dtd)
+ locale/browser/browser.dtd (%chrome/browser.dtd)
+ locale/browser/browser.properties (%chrome/browser.properties)
+ locale/browser/config.dtd (%chrome/config.dtd)
+ locale/browser/preferences.dtd (%chrome/preferences.dtd)
+ locale/browser/checkbox.dtd (%chrome/checkbox.dtd)
+ locale/browser/notification.dtd (%chrome/notification.dtd)
+ locale/browser/sync.dtd (%chrome/sync.dtd)
+ locale/browser/sync.properties (%chrome/sync.properties)
+ locale/browser/prompt.dtd (%chrome/prompt.dtd)
+ locale/browser/phishing.dtd (%chrome/phishing.dtd)
+
+@AB_CD@.jar:
+% locale browser @AB_CD@ %locale/browser/
+ locale/browser/bookmarks.json (bookmarks.json)
+* locale/browser/passwordmgr.properties (%overrides/passwordmgr.properties)
+% override chrome://passwordmgr/locale/passwordmgr.properties chrome://browser/locale/passwordmgr.properties
+
+#
+# Browser jar resources
+#
+
+@AB_CD@.jar:
+relativesrcdir browser/locales:
+ locale/browser/region.properties (%chrome/browser-region/region.properties)
+* locale/browser/netError.dtd (%chrome/overrides/netError.dtd)
+% override chrome://global/locale/netError.dtd chrome://browser/locale/netError.dtd
+* locale/browser/appstrings.properties (%chrome/overrides/appstrings.properties)
+% override chrome://global/locale/appstrings.properties chrome://browser/locale/appstrings.properties
diff --git a/browser/metro/metroapp.ini.in b/browser/metro/metroapp.ini.in
new file mode 100644
index 000000000000..2b7867ba1f71
--- /dev/null
+++ b/browser/metro/metroapp.ini.in
@@ -0,0 +1,22 @@
+#filter substitution
+[App]
+Vendor=Mozilla
+Name=MetroFirefox
+UAName=Firefox
+Version=@GRE_MILESTONE@
+BuildID=@GRE_BUILDID@
+ID={99bceaaa-e3c6-48c1-b981-ef9b46b67d60}
+
+[Gecko]
+MinVersion=@GRE_MILESTONE@
+MaxVersion=@GRE_MILESTONE@
+
+[XRE]
+EnableExtensionManager=0
+EnableProfileMigrator=0
+
+[Crash Reporter]
+#if MOZILLA_OFFICIAL
+Enabled=1
+#endif
+ServerURL=https://crash-reports.mozilla.com/submit?id={99bceaaa-e3c6-48c1-b981-ef9b46b67d60}&version=@GRE_MILESTONE@&buildid=@GRE_BUILDID@
diff --git a/browser/metro/modules/Makefile.in b/browser/metro/modules/Makefile.in
new file mode 100644
index 000000000000..52584df83967
--- /dev/null
+++ b/browser/metro/modules/Makefile.in
@@ -0,0 +1,17 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEPTH = @DEPTH@
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+include $(topsrcdir)/config/config.mk
+
+EXTRA_JS_MODULES = \
+ video.jsm \
+ $(NULL)
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/metro/modules/video.jsm b/browser/metro/modules/video.jsm
new file mode 100644
index 000000000000..10e142b49175
--- /dev/null
+++ b/browser/metro/modules/video.jsm
@@ -0,0 +1,9 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+this.EXPORTED_SYMBOLS = ["Video"];
+
+this.Video = {
+ fullScreenSourceElement: null
+};
diff --git a/browser/metro/profile/Makefile.in b/browser/metro/profile/Makefile.in
new file mode 100644
index 000000000000..834a723802f5
--- /dev/null
+++ b/browser/metro/profile/Makefile.in
@@ -0,0 +1,15 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this file,
+# You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEPTH = ../../..
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+include $(topsrcdir)/config/config.mk
+
+PREF_JS_EXPORTS = $(srcdir)/metro.js
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/metro/profile/metro.js b/browser/metro/profile/metro.js
new file mode 100644
index 000000000000..a756ea802420
--- /dev/null
+++ b/browser/metro/profile/metro.js
@@ -0,0 +1,583 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#filter substitution
+
+#ifdef DEBUG
+// disable content and content script caching
+pref("nglayout.debug.disable_xul_cache", true);
+pref("nglayout.debug.disable_xul_fastload", true);
+pref("devtools.errorconsole.enabled", true);
+#endif
+
+// Enable headless crash reporting by default
+pref("app.reportCrashes", true);
+
+// Debug prefs, see input.js
+pref("metro.debug.treatmouseastouch", false);
+pref("metro.debug.colorizeInputOverlay", false);
+pref("metro.debug.selection.displayRanges", false);
+pref("metro.debug.selection.dumpRanges", false);
+pref("metro.debug.selection.dumpEvents", false);
+
+// Form helper options: 0 = disabled, 1 = enabled, 2 = dynamic depending on screen size
+pref("formhelper.mode", 1);
+// Auto zoom to form elements when they take focus
+pref("formhelper.autozoom", true);
+// Auto zoom to the caret
+pref("formhelper.autozoom.caret", false);
+
+// form autocomplete service
+pref("browser.formfill.enable", true);
+
+// Enable Microsoft TSF support by default for imes.
+pref("intl.enable_tsf_support", true);
+
+pref("general.autoScroll", true);
+pref("general.smoothScroll", true);
+pref("general.smoothScroll.durationToIntervalRatio", 200);
+pref("mousewheel.enable_pixel_scrolling", true);
+
+// For browser.xml binding
+//
+// cacheRatio* is a ratio that determines the amount of pixels to cache. The
+// ratio is multiplied by the viewport width or height to get the displayport's
+// width or height, respectively.
+//
+// (divide integer value by 1000 to get the ratio)
+//
+// For instance: cachePercentageWidth is 1500
+// viewport height is 500
+// => display port height will be 500 * 1.5 = 750
+//
+pref("toolkit.browser.cacheRatioWidth", 2000);
+pref("toolkit.browser.cacheRatioHeight", 3000);
+
+// How long before a content view (a handle to a remote scrollable object)
+// expires.
+pref("toolkit.browser.contentViewExpire", 3000);
+
+pref("toolkit.defaultChromeURI", "chrome://browser/content/browser.xul");
+pref("browser.chromeURL", "chrome://browser/content/");
+
+// When true, always show the tab strip and use desktop-style tabs (no thumbnails)
+pref("browser.tabs.tabsOnly", false);
+
+pref("browser.tabs.warnOnClose", true);
+pref("browser.tabs.remote", false);
+
+// Telemetry
+pref("toolkit.telemetry.enabled", true);
+pref("toolkit.telemetry.prompted", 2);
+
+pref("toolkit.screen.lock", false);
+
+// From libpref/src/init/all.js, extended to allow a slightly wider zoom range.
+pref("zoom.minPercent", 20);
+pref("zoom.maxPercent", 400);
+pref("toolkit.zoomManager.zoomValues", ".2,.3,.5,.67,.8,.9,1,1.1,1.2,1.33,1.5,1.7,2,2.4,3,4");
+
+// Device pixel to CSS px ratio, in percent. Set to -1 to calculate based on display density.
+pref("browser.viewport.scaleRatio", -1);
+
+/* use long press to display a context menu */
+pref("ui.click_hold_context_menus", false);
+
+/* offline cache prefs */
+pref("browser.offline-apps.notify", true);
+
+/* protocol warning prefs */
+pref("network.protocol-handler.warn-external.tel", false);
+pref("network.protocol-handler.warn-external.mailto", false);
+pref("network.protocol-handler.warn-external.vnd.youtube", false);
+
+/* history max results display */
+pref("browser.display.history.maxresults", 100);
+
+/* max items per section of the startui */
+pref("browser.display.startUI.maxresults", 16);
+
+// Backspace and Shift+Backspace behavior
+// 0 goes Back/Forward
+// 1 act like PgUp/PgDown
+// 2 and other values, nothing
+pref("browser.backspace_action", 0);
+
+/* session history */
+pref("browser.sessionhistory.max_entries", 50);
+
+// On startup, automatically restore tabs from last time?
+pref("browser.startup.sessionRestore", false);
+
+/* session store */
+pref("browser.sessionstore.resume_from_crash", true);
+pref("browser.sessionstore.resume_session_once", false);
+pref("browser.sessionstore.resume_from_crash_timeout", 60); // minutes
+// minimal interval between two save operations in milliseconds
+pref("browser.sessionstore.interval", 15000); // milliseconds
+// maximum amount of POSTDATA to be saved in bytes per history entry (-1 = all of it)
+// (NB: POSTDATA will be saved either entirely or not at all)
+pref("browser.sessionstore.postdata", 0);
+// on which sites to save text data, POSTDATA and cookies
+// 0 = everywhere, 1 = unencrypted sites, 2 = nowhere
+pref("browser.sessionstore.privacy_level", 0);
+// the same as browser.sessionstore.privacy_level, but for saving deferred session data
+pref("browser.sessionstore.privacy_level_deferred", 1);
+// how many tabs can be reopened (per window)
+pref("browser.sessionstore.max_tabs_undo", 10);
+// number of crashes that can occur before the about:sessionrestore page is displayed
+// (this pref has no effect if more than 6 hours have passed since the last crash)
+pref("browser.sessionstore.max_resumed_crashes", 1);
+// restore_on_demand overrides MAX_CONCURRENT_TAB_RESTORES (sessionstore constant)
+// and restore_hidden_tabs. When true, tabs will not be restored until they are
+// focused (also applies to tabs that aren't visible). When false, the values
+// for MAX_CONCURRENT_TAB_RESTORES and restore_hidden_tabs are respected.
+// Selected tabs are always restored regardless of this pref.
+pref("browser.sessionstore.restore_on_demand", true);
+
+/* these should help performance */
+pref("mozilla.widget.force-24bpp", true);
+pref("mozilla.widget.use-buffer-pixmap", true);
+pref("mozilla.widget.disable-native-theme", false);
+pref("layout.reflow.synthMouseMove", false);
+
+/* "Preview" of framerate increase for animations, discussed in 710563. */
+pref("layout.frame_rate.precise", true);
+
+/* download manager (don't show the window or alert) */
+pref("browser.download.useDownloadDir", true);
+pref("browser.download.folderList", 1); // Default to ~/Downloads
+pref("browser.download.manager.showAlertOnComplete", false);
+pref("browser.download.manager.showAlertInterval", 2000);
+pref("browser.download.manager.retention", 2);
+pref("browser.download.manager.showWhenStarting", false);
+pref("browser.download.manager.closeWhenDone", true);
+pref("browser.download.manager.openDelay", 0);
+pref("browser.download.manager.focusWhenStarting", false);
+pref("browser.download.manager.flashCount", 2);
+pref("browser.download.manager.addToRecentDocs", true);
+pref("browser.download.manager.displayedHistoryDays", 7);
+pref("browser.download.manager.resumeOnWakeDelay", 10000);
+pref("browser.download.manager.quitBehavior", 0);
+
+/* download alerts (disabled above) */
+pref("alerts.totalOpenTime", 6000);
+
+/* download helper */
+pref("browser.helperApps.deleteTempFileOnExit", false);
+
+/* password manager */
+pref("signon.rememberSignons", true);
+pref("signon.expireMasterPassword", false);
+pref("signon.SignonFileName", "signons.txt");
+
+/* find helper */
+pref("findhelper.autozoom", true);
+
+// this will automatically enable inline spellchecking (if it is available) for
+// editable elements in HTML
+// 0 = spellcheck nothing
+// 1 = check multi-line controls [default]
+// 2 = check multi/single line controls
+pref("layout.spellcheckDefault", 1);
+
+/* extension manager and xpinstall */
+// Disable all add-on locations other than the profile
+pref("extensions.enabledScopes", 1);
+// Auto-disable any add-ons that are "dropped in" to the profile
+pref("extensions.autoDisableScopes", 1);
+// Disable add-on installation via the web-exposed APIs
+pref("xpinstall.enabled", false);
+pref("xpinstall.whitelist.add", "addons.mozilla.org");
+pref("extensions.autoupdate.enabled", false);
+pref("extensions.update.enabled", false);
+
+/* blocklist preferences */
+pref("extensions.blocklist.enabled", true);
+pref("extensions.blocklist.interval", 86400);
+pref("extensions.blocklist.url", "https://addons.mozilla.org/blocklist/3/%APP_ID%/%APP_VERSION%/%PRODUCT%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/%PING_COUNT%/%TOTAL_PING_COUNT%/%DAYS_SINCE_LAST_PING%/");
+pref("extensions.blocklist.detailsURL", "https://www.mozilla.com/%LOCALE%/blocklist/");
+
+/* block popups by default, and notify the user about blocked popups */
+pref("dom.disable_open_during_load", true);
+pref("privacy.popups.showBrowserMessage", true);
+
+/* disable opening windows with the dialog feature */
+pref("dom.disable_window_open_dialog_feature", true);
+
+pref("keyword.enabled", true);
+pref("keyword.URL", "http://www.bing.com/search?q=");
+
+pref("accessibility.typeaheadfind", false);
+pref("accessibility.typeaheadfind.timeout", 5000);
+pref("accessibility.typeaheadfind.flashBar", 1);
+pref("accessibility.typeaheadfind.linksonly", false);
+pref("accessibility.typeaheadfind.casesensitive", 0);
+
+// Trun on F7 caret browsing hot key
+pref("accessibility.browsewithcaret_shortcut.enabled", true);
+pref("accessibility.browsewithcaret", false);
+
+// Whether or not we show a dialog box informing the user that the update was
+// successfully applied.
+pref("app.update.showInstalledUI", false);
+
+// Whether the character encoding menu is under the main Firefox button. This
+// preference is a string so that localizers can alter it.
+pref("browser.menu.showCharacterEncoding", "chrome://browser/locale/browser.properties");
+pref("intl.charsetmenu.browser.static", "chrome://browser/locale/browser.properties");
+
+// pointer to the default engine name
+pref("browser.search.defaultenginename", "chrome://browser/locale/browser.properties");
+
+// SSL error page behaviour
+pref("browser.ssl_override_behavior", 2);
+pref("browser.xul.error_pages.expert_bad_cert", false);
+
+// disable logging for the search service by default
+pref("browser.search.log", false);
+
+// ordering of search engines in the engine list.
+pref("browser.search.order.1", "chrome://browser/locale/browser.properties");
+pref("browser.search.order.2", "chrome://browser/locale/browser.properties");
+pref("browser.search.order.3", "chrome://browser/locale/browser.properties");
+
+// send ping to the server to update
+pref("browser.search.update", true);
+
+// disable logging for the search service update system by default
+pref("browser.search.update.log", false);
+
+// Check whether we need to perform engine updates every 6 hours
+pref("browser.search.update.interval", 21600);
+
+// enable search suggestions by default
+pref("browser.search.suggest.enabled", true);
+
+// tell the search service that we don't really expose the "current engine"
+pref("browser.search.noCurrentEngine", true);
+
+#ifdef MOZ_OFFICIAL_BRANDING
+// {moz:official} expands to "official"
+pref("browser.search.official", true);
+#endif
+
+// enable xul error pages
+pref("browser.xul.error_pages.enabled", true);
+
+// Specify emptyRestriction = 0 so that bookmarks appear in the list by default
+pref("browser.urlbar.default.behavior", 0);
+pref("browser.urlbar.default.behavior.emptyRestriction", 0);
+
+// Let the faviconservice know that we display favicons as 25x25px so that it
+// uses the right size when optimizing favicons
+pref("places.favicons.optimizeToDimension", 25);
+
+// various and sundry awesomebar prefs (should remove/re-evaluate
+// these once bug 447900 is fixed)
+pref("browser.urlbar.clickSelectsAll", true);
+pref("browser.urlbar.doubleClickSelectsAll", true);
+pref("browser.urlbar.autoFill", false);
+pref("browser.urlbar.matchOnlyTyped", false);
+pref("browser.urlbar.matchBehavior", 1);
+pref("browser.urlbar.filter.javascript", true);
+pref("browser.urlbar.maxRichResults", 8);
+pref("browser.urlbar.search.chunkSize", 1000);
+pref("browser.urlbar.search.timeout", 100);
+pref("browser.urlbar.restrict.history", "^");
+pref("browser.urlbar.restrict.bookmark", "*");
+pref("browser.urlbar.restrict.tag", "+");
+pref("browser.urlbar.match.title", "#");
+pref("browser.urlbar.match.url", "@");
+pref("browser.history.grouping", "day");
+pref("browser.history.showSessions", false);
+pref("browser.sessionhistory.max_entries", 50);
+pref("browser.history_expire_days", 180);
+pref("browser.history_expire_days_min", 90);
+pref("browser.history_expire_sites", 40000);
+pref("browser.places.migratePostDataAnnotations", true);
+pref("browser.places.updateRecentTagsUri", true);
+pref("places.frecency.numVisits", 10);
+pref("places.frecency.numCalcOnIdle", 50);
+pref("places.frecency.numCalcOnMigrate", 50);
+pref("places.frecency.updateIdleTime", 60000);
+pref("places.frecency.firstBucketCutoff", 4);
+pref("places.frecency.secondBucketCutoff", 14);
+pref("places.frecency.thirdBucketCutoff", 31);
+pref("places.frecency.fourthBucketCutoff", 90);
+pref("places.frecency.firstBucketWeight", 100);
+pref("places.frecency.secondBucketWeight", 70);
+pref("places.frecency.thirdBucketWeight", 50);
+pref("places.frecency.fourthBucketWeight", 30);
+pref("places.frecency.defaultBucketWeight", 10);
+pref("places.frecency.embedVisitBonus", 0);
+pref("places.frecency.linkVisitBonus", 100);
+pref("places.frecency.typedVisitBonus", 2000);
+pref("places.frecency.bookmarkVisitBonus", 150);
+pref("places.frecency.downloadVisitBonus", 0);
+pref("places.frecency.permRedirectVisitBonus", 0);
+pref("places.frecency.tempRedirectVisitBonus", 0);
+pref("places.frecency.defaultVisitBonus", 0);
+pref("places.frecency.unvisitedBookmarkBonus", 140);
+pref("places.frecency.unvisitedTypedBonus", 200);
+
+// disable color management
+pref("gfx.color_management.mode", 0);
+
+// don't allow JS to move and resize existing windows
+pref("dom.disable_window_move_resize", true);
+
+// prevent click image resizing for nsImageDocument
+pref("browser.enable_click_image_resizing", false);
+
+// open in tab preferences
+// 0=default window, 1=current window/tab, 2=new window, 3=new tab in most window
+pref("browser.link.open_external", 3);
+pref("browser.link.open_newwindow", 3);
+// 0=force all new windows to tabs, 1=don't force, 2=only force those with no features set
+pref("browser.link.open_newwindow.restriction", 0);
+
+// controls which bits of private data to clear. by default we clear them all.
+pref("privacy.item.cache", true);
+pref("privacy.item.cookies", true);
+pref("privacy.item.offlineApps", true);
+pref("privacy.item.history", true);
+pref("privacy.item.formdata", true);
+pref("privacy.item.downloads", true);
+pref("privacy.item.passwords", true);
+pref("privacy.item.sessions", true);
+pref("privacy.item.geolocation", true);
+pref("privacy.item.siteSettings", true);
+pref("privacy.item.syncAccount", true);
+
+pref("plugins.force.wmode", "opaque");
+
+// What default should we use for the time span in the sanitizer:
+// 0 - Clear everything
+// 1 - Last Hour
+// 2 - Last 2 Hours
+// 3 - Last 4 Hours
+// 4 - Today
+pref("privacy.sanitize.timeSpan", 1);
+pref("privacy.sanitize.sanitizeOnShutdown", false);
+pref("privacy.sanitize.migrateFx3Prefs", false);
+
+// URL to the Learn More link XXX this is the firefox one. Bug 495578 fixes this.
+pref("browser.geolocation.warning.infoURL", "http://www.mozilla.com/%LOCALE%/firefox/geolocation/");
+
+// enable geo
+pref("geo.enabled", true);
+
+// JS error console
+pref("devtools.errorconsole.enabled", false);
+
+// kinetic tweakables
+pref("browser.ui.kinetic.updateInterval", 16);
+pref("browser.ui.kinetic.exponentialC", 1400);
+pref("browser.ui.kinetic.polynomialC", 100);
+pref("browser.ui.kinetic.swipeLength", 160);
+pref("browser.ui.zoom.animationDuration", 200); // ms duration of double-tap zoom animation
+
+// pinch gesture
+pref("browser.ui.pinch.maxGrowth", 150); // max pinch distance growth
+pref("browser.ui.pinch.maxShrink", 200); // max pinch distance shrinkage
+pref("browser.ui.pinch.scalingFactor", 500); // scaling factor for above pinch limits
+
+pref("ui.mouse.radius.enabled", true);
+pref("ui.touch.radius.enabled", true);
+
+// plugins
+pref("plugin.disable", true);
+pref("dom.ipc.plugins.enabled", true);
+
+// process priority
+// higher values give content process less CPU time
+pref("dom.ipc.content.nice", 1);
+
+// product URLs
+// The breakpad report server to link to in about:crashes
+pref("breakpad.reportURL", "http://crash-stats.mozilla.com/report/index/");
+pref("app.releaseNotesURL", "http://www.mozilla.com/%LOCALE%/mobile/%VERSION%/releasenotes/");
+pref("app.sync.tutorialURL", "https://support.mozilla.org/kb/sync-firefox-between-desktop-and-mobile");
+pref("app.support.baseURL", "http://support.mozilla.org/1/firefox/%VERSION%/%OS%/%LOCALE%/");
+pref("app.feedbackURL", "http://input.mozilla.com/feedback/");
+pref("app.privacyURL", "http://www.mozilla.com/legal/privacy/");
+pref("app.creditsURL", "http://www.mozilla.org/credits/");
+pref("app.channelURL", "http://www.mozilla.org/%LOCALE%/firefox/channel/");
+#if MOZ_UPDATE_CHANNEL == beta
+pref("app.faqURL", "http://www.mozilla.com/%LOCALE%/mobile/beta/faq/");
+#else
+pref("app.faqURL", "http://www.mozilla.com/%LOCALE%/mobile/faq/");
+#endif
+
+// Name of alternate about: page for certificate errors (when undefined, defaults to about:neterror)
+pref("security.alternate_certificate_error_page", "certerror");
+
+pref("security.warn_viewing_mixed", false); // Warning is disabled. See Bug 616712.
+
+// Override some named colors to avoid inverse OS themes
+
+#ifdef MOZ_OFFICIAL_BRANDING
+pref("browser.search.param.yahoo-fr", "moz35");
+pref("browser.search.param.yahoo-fr-cjkt", "moz35");
+pref("browser.search.param.yahoo-fr-ja", "mozff");
+#endif
+
+/* app update prefs */
+pref("app.update.timer", 60000); // milliseconds (1 min)
+
+#ifdef MOZ_UPDATER
+// temp
+pref("app.update.enabled", false);
+pref("app.update.timerFirstInterval", 20000); // milliseconds
+pref("app.update.auto", false);
+pref("app.update.channel", "@MOZ_UPDATE_CHANNEL@");
+pref("app.update.mode", 1);
+pref("app.update.silent", false);
+pref("app.update.url", "https://aus2.mozilla.org/update/4/%PRODUCT%/%VERSION%/%BUILD_ID%/%BUILD_TARGET%-xul/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/%PLATFORM_VERSION%/update.xml");
+pref("app.update.promptWaitTime", 43200);
+pref("app.update.idletime", 60);
+pref("app.update.showInstalledUI", false);
+pref("app.update.incompatible.mode", 0);
+pref("app.update.download.backgroundInterval", 0);
+
+#ifdef MOZ_OFFICIAL_BRANDING
+pref("app.update.interval", 86400);
+pref("app.update.url.manual", "http://www.mozilla.com/%LOCALE%/m/");
+pref("app.update.url.details", "http://www.mozilla.com/%LOCALE%/mobile/releases/");
+#else
+pref("app.update.interval", 28800);
+pref("app.update.url.manual", "http://www.mozilla.com/%LOCALE%/mobile/");
+pref("app.update.url.details", "http://www.mozilla.com/%LOCALE%/mobile/");
+#endif
+#endif
+
+// replace newlines with spaces on paste into single-line text boxes
+pref("editor.singleLine.pasteNewlines", 2);
+
+#ifdef MOZ_SERVICES_SYNC
+// sync service
+pref("services.sync.registerEngines", "Tab,Bookmarks,Form,History,Password,Prefs");
+pref("services.sync.autoconnectDelay", 5);
+
+// prefs to sync by default
+pref("services.sync.prefs.sync.browser.startup.sessionRestore", true);
+pref("services.sync.prefs.sync.browser.tabs.warnOnClose", true);
+pref("services.sync.prefs.sync.devtools.errorconsole.enabled", true);
+pref("services.sync.prefs.sync.lightweightThemes.isThemeSelected", true);
+pref("services.sync.prefs.sync.lightweightThemes.usedThemes", true);
+pref("services.sync.prefs.sync.privacy.donottrackheader.enabled", true);
+pref("services.sync.prefs.sync.privacy.donottrackheader.value", true);
+pref("services.sync.prefs.sync.signon.rememberSignons", true);
+#endif
+
+// threshold where a tap becomes a drag, in 1/240" reference pixels
+// The names of the preferences are to be in sync with nsEventStateManager.cpp
+pref("ui.dragThresholdX", 50);
+pref("ui.dragThresholdY", 50);
+
+pref("notification.feature.enabled", true);
+
+// prevent tooltips from showing up
+pref("browser.chrome.toolbar_tips", false);
+
+// 0: don't show fullscreen keyboard
+// 1: always show fullscreen keyboard
+// -1: show fullscreen keyboard based on threshold pref
+pref("widget.ime.android.landscape_fullscreen", -1);
+pref("widget.ime.android.fullscreen_threshold", 250); // in hundreths of inches
+
+// Completely disable pdf.js as an option to preview pdfs within firefox.
+// Note: if this is not disabled it does not necessarily mean pdf.js is the pdf
+// handler just that it is an option.
+pref("pdfjs.disabled", false);
+// Used by pdf.js to know the first time firefox is run with it installed so it
+// can become the default pdf viewer.
+pref("pdfjs.firstRun", true);
+// The values of preferredAction and alwaysAskBeforeHandling before pdf.js
+// became the default.
+pref("pdfjs.previousHandler.preferredAction", 0);
+pref("pdfjs.previousHandler.alwaysAskBeforeHandling", false);
+
+// The maximum amount of decoded image data we'll willingly keep around (we
+// might keep around more than this, but we'll try to get down to this value).
+// (This is intentionally on the high side; see bug 746055.)
+pref("image.mem.max_decoded_image_kb", 256000);
+
+// enable touch events interfaces
+pref("dom.w3c_touch_events.enabled", 1);
+pref("dom.w3c_touch_events.safetyX", 5); // escape borders in units of 1/240"
+pref("dom.w3c_touch_events.safetyY", 20); // escape borders in units of 1/240"
+
+#ifdef MOZ_SAFE_BROWSING
+// Safe browsing does nothing unless this pref is set
+pref("browser.safebrowsing.enabled", true);
+
+// Prevent loading of pages identified as malware
+pref("browser.safebrowsing.malware.enabled", true);
+
+// Non-enhanced mode (local url lists) URL list to check for updates
+pref("browser.safebrowsing.provider.0.updateURL", "http://safebrowsing.clients.google.com/safebrowsing/downloads?client={moz:client}&appver={moz:version}&pver=2.2");
+
+pref("browser.safebrowsing.dataProvider", 0);
+
+// Does the provider name need to be localizable?
+pref("browser.safebrowsing.provider.0.name", "Google");
+pref("browser.safebrowsing.provider.0.keyURL", "https://sb-ssl.google.com/safebrowsing/newkey?client={moz:client}&appver={moz:version}&pver=2.2");
+pref("browser.safebrowsing.provider.0.reportURL", "http://safebrowsing.clients.google.com/safebrowsing/report?");
+pref("browser.safebrowsing.provider.0.gethashURL", "http://safebrowsing.clients.google.com/safebrowsing/gethash?client={moz:client}&appver={moz:version}&pver=2.2");
+
+// HTML report pages
+pref("browser.safebrowsing.provider.0.reportGenericURL", "http://{moz:locale}.phish-generic.mozilla.com/?hl={moz:locale}");
+pref("browser.safebrowsing.provider.0.reportErrorURL", "http://{moz:locale}.phish-error.mozilla.com/?hl={moz:locale}");
+pref("browser.safebrowsing.provider.0.reportPhishURL", "http://{moz:locale}.phish-report.mozilla.com/?hl={moz:locale}");
+pref("browser.safebrowsing.provider.0.reportMalwareURL", "http://{moz:locale}.malware-report.mozilla.com/?hl={moz:locale}");
+pref("browser.safebrowsing.provider.0.reportMalwareErrorURL", "http://{moz:locale}.malware-error.mozilla.com/?hl={moz:locale}");
+
+// FAQ URLs
+pref("browser.safebrowsing.warning.infoURL", "http://www.mozilla.com/%LOCALE%/%APP%/phishing-protection/");
+pref("browser.geolocation.warning.infoURL", "http://www.mozilla.com/%LOCALE%/%APP%/geolocation/");
+
+// Name of the about: page contributed by safebrowsing to handle display of error
+// pages on phishing/malware hits. (bug 399233)
+pref("urlclassifier.alternate_error_page", "blocked");
+
+// The number of random entries to send with a gethash request.
+pref("urlclassifier.gethashnoise", 4);
+
+// The list of tables that use the gethash request to confirm partial results.
+pref("urlclassifier.gethashtables", "goog-phish-shavar,goog-malware-shavar");
+
+// If an urlclassifier table has not been updated in this number of seconds,
+// a gethash request will be forced to check that the result is still in
+// the database.
+pref("urlclassifier.max-complete-age", 2700);
+
+// Maximum size of the sqlite3 cache during an update, in bytes
+pref("urlclassifier.updatecachemax", 41943040);
+
+// URL for checking the reason for a malware warning.
+pref("browser.safebrowsing.malware.reportURL", "http://safebrowsing.clients.google.com/safebrowsing/diagnostic?client=%NAME%&hl=%LOCALE%&site=");
+#endif
+
+// True if this is the first time we are showing about:firstrun
+pref("browser.firstrun.show.localepicker", false);
+
+// True if you always want dump() to work
+//
+// On Android, you also need to do the following for the output
+// to show up in logcat:
+//
+// $ adb shell stop
+// $ adb shell setprop log.redirect-stdio true
+// $ adb shell start
+pref("javascript.options.showInConsole", true);
+pref("browser.dom.window.dump.enabled", true);
+
+// controls if we want camera support
+pref("device.camera.enabled", true);
+pref("media.realtime_decoder.enabled", true);
+
+// Mobile manages state by autodetection
+pref("network.manage-offline-status", true);
diff --git a/browser/metro/shell/Makefile.in b/browser/metro/shell/Makefile.in
new file mode 100644
index 000000000000..83541fff4ac6
--- /dev/null
+++ b/browser/metro/shell/Makefile.in
@@ -0,0 +1,33 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEPTH = ../../..
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+MODULE = metro
+
+include $(DEPTH)/config/autoconf.mk
+
+DIRS = commandexecutehandler linktool
+
+ifdef ENABLE_TESTS
+DIRS += testing
+endif
+
+export::
+ $(NSINSTALL) $(srcdir)/resources.pri $(DIST)/bin
+ $(RM) $(DIST)/bin/VisualElementsManifest.xml
+ $(PYTHON) $(topsrcdir)/config/Preprocessor.py -Fsubstitution $(DEFINES) $(ACDEFINES) -DMOZ_APP_DISPLAYNAME=${MOZ_APP_DISPLAYNAME} \
+ $(srcdir)/VisualElementsManifest.xml.in > $(DIST)/bin/VisualElementsManifest.xml
+
+install::
+ $(NSINSTALL) $(srcdir)/resources.pri $(DIST)/bin
+
+# bug 744566
+# $(RM) $(DIST)/bin/resources.pri
+# $(MAKEPRI) new -v -pr $(srcdir)/tileresources -cf $(srcdir)/priconfig.xml -mn $(srcdir)/AppManifest.xml -of $(DIST)/bin/resources.pri -o
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/metro/shell/VisualElementsManifest.xml.in b/browser/metro/shell/VisualElementsManifest.xml.in
new file mode 100644
index 000000000000..a549ae4b1722
--- /dev/null
+++ b/browser/metro/shell/VisualElementsManifest.xml.in
@@ -0,0 +1,16 @@
+
+
+
+
+
+
diff --git a/browser/metro/shell/commandexecutehandler/CEHHelper.cpp b/browser/metro/shell/commandexecutehandler/CEHHelper.cpp
new file mode 100644
index 000000000000..46bd046ce754
--- /dev/null
+++ b/browser/metro/shell/commandexecutehandler/CEHHelper.cpp
@@ -0,0 +1,166 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "CEHHelper.h"
+
+HANDLE sCon;
+LPCWSTR metroDX10Available = L"MetroD3DAvailable";
+
+typedef HRESULT (WINAPI*D3D10CreateDevice1Func)
+ (IDXGIAdapter *, D3D10_DRIVER_TYPE, HMODULE, UINT,
+ D3D10_FEATURE_LEVEL1, UINT, ID3D10Device1 **);
+typedef HRESULT(WINAPI*CreateDXGIFactory1Func)(REFIID , void **);
+
+void
+Log(const wchar_t *fmt, ...)
+{
+#if !defined(SHOW_CONSOLE)
+ return;
+#endif
+ va_list a = NULL;
+ wchar_t szDebugString[1024];
+ if(!lstrlenW(fmt))
+ return;
+ va_start(a,fmt);
+ vswprintf(szDebugString, 1024, fmt, a);
+ va_end(a);
+ if(!lstrlenW(szDebugString))
+ return;
+
+ DWORD len;
+ WriteConsoleW(sCon, szDebugString, lstrlenW(szDebugString), &len, NULL);
+ WriteConsoleW(sCon, L"\n", 1, &len, NULL);
+
+ if (IsDebuggerPresent()) {
+ OutputDebugStringW(szDebugString);
+ OutputDebugStringW(L"\n");
+ }
+}
+
+#if defined(SHOW_CONSOLE)
+void
+SetupConsole()
+{
+ FILE *fp;
+ AllocConsole();
+ sCon = GetStdHandle(STD_OUTPUT_HANDLE);
+ int fd = _open_osfhandle(reinterpret_cast(sCon), 0);
+ fp = _fdopen(fd, "w");
+ *stdout = *fp;
+ setvbuf(stdout, NULL, _IONBF, 0);
+}
+#endif
+
+bool
+IsDX10Available()
+{
+ DWORD isDX10Available;
+ if (GetDWORDRegKey(metroDX10Available, isDX10Available)) {
+ return isDX10Available;
+ }
+
+ HMODULE dxgiModule = LoadLibraryA("dxgi.dll");
+ if (!dxgiModule) {
+ SetDWORDRegKey(metroDX10Available, 0);
+ return false;
+ }
+ CreateDXGIFactory1Func createDXGIFactory1 =
+ (CreateDXGIFactory1Func) GetProcAddress(dxgiModule, "CreateDXGIFactory1");
+ if (!createDXGIFactory1) {
+ SetDWORDRegKey(metroDX10Available, 0);
+ return false;
+ }
+
+ HMODULE d3d10module = LoadLibraryA("d3d10_1.dll");
+ if (!d3d10module) {
+ SetDWORDRegKey(metroDX10Available, 0);
+ return false;
+ }
+ D3D10CreateDevice1Func createD3DDevice =
+ (D3D10CreateDevice1Func) GetProcAddress(d3d10module,
+ "D3D10CreateDevice1");
+ if (!createD3DDevice) {
+ SetDWORDRegKey(metroDX10Available, 0);
+ return false;
+ }
+
+ CComPtr factory1;
+ if (FAILED(createDXGIFactory1(__uuidof(IDXGIFactory1), (void**)&factory1))) {
+ SetDWORDRegKey(metroDX10Available, 0);
+ return false;
+ }
+
+ CComPtr adapter1;
+ if (FAILED(factory1->EnumAdapters1(0, &adapter1))) {
+ SetDWORDRegKey(metroDX10Available, 0);
+ return false;
+ }
+
+ CComPtr device;
+ // Try for DX10.1
+ if (FAILED(createD3DDevice(adapter1, D3D10_DRIVER_TYPE_HARDWARE, NULL,
+ D3D10_CREATE_DEVICE_BGRA_SUPPORT |
+ D3D10_CREATE_DEVICE_PREVENT_INTERNAL_THREADING_OPTIMIZATIONS,
+ D3D10_FEATURE_LEVEL_10_1,
+ D3D10_1_SDK_VERSION, &device))) {
+ // Try for DX10
+ if (FAILED(createD3DDevice(adapter1, D3D10_DRIVER_TYPE_HARDWARE, NULL,
+ D3D10_CREATE_DEVICE_BGRA_SUPPORT |
+ D3D10_CREATE_DEVICE_PREVENT_INTERNAL_THREADING_OPTIMIZATIONS,
+ D3D10_FEATURE_LEVEL_10_0,
+ D3D10_1_SDK_VERSION, &device))) {
+ // Try for DX9.3 (we fall back to cairo and cairo has support for D3D 9.3)
+ if (FAILED(createD3DDevice(adapter1, D3D10_DRIVER_TYPE_HARDWARE, NULL,
+ D3D10_CREATE_DEVICE_BGRA_SUPPORT |
+ D3D10_CREATE_DEVICE_PREVENT_INTERNAL_THREADING_OPTIMIZATIONS,
+ D3D10_FEATURE_LEVEL_9_3,
+ D3D10_1_SDK_VERSION, &device))) {
+
+ SetDWORDRegKey(metroDX10Available, 0);
+ return false;
+ }
+ }
+ }
+
+
+ SetDWORDRegKey(metroDX10Available, 1);
+ return true;
+}
+
+bool
+GetDWORDRegKey(LPCWSTR name, DWORD &value)
+{
+ HKEY key;
+ LONG result = RegOpenKeyExW(HKEY_CURRENT_USER, L"SOFTWARE\\Mozilla\\Firefox",
+ 0, KEY_READ, &key);
+ if (result != ERROR_SUCCESS) {
+ return false;
+ }
+
+ DWORD bufferSize = sizeof(DWORD);
+ DWORD type;
+ result = RegQueryValueExW(key, name, nullptr, &type,
+ reinterpret_cast(&value),
+ &bufferSize);
+ RegCloseKey(key);
+ return result == ERROR_SUCCESS;
+}
+
+bool
+SetDWORDRegKey(LPCWSTR name, DWORD value)
+{
+ HKEY key;
+ LONG result = RegOpenKeyExW(HKEY_CURRENT_USER, L"SOFTWARE\\Mozilla\\Firefox",
+ 0, KEY_WRITE, &key);
+ if (result != ERROR_SUCCESS) {
+ return false;
+ }
+
+ result = RegSetValueEx(key, name, 0, REG_DWORD,
+ reinterpret_cast(&value),
+ sizeof(DWORD));
+ RegCloseKey(key);
+ return result == ERROR_SUCCESS;
+}
diff --git a/browser/metro/shell/commandexecutehandler/CEHHelper.h b/browser/metro/shell/commandexecutehandler/CEHHelper.h
new file mode 100644
index 000000000000..b95e75148724
--- /dev/null
+++ b/browser/metro/shell/commandexecutehandler/CEHHelper.h
@@ -0,0 +1,29 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#undef WINVER
+#undef _WIN32_WINNT
+#define WINVER 0x602
+#define _WIN32_WINNT 0x602
+
+#include
+#include
+#include
+#include
+#include
+
+//#define SHOW_CONSOLE 1
+extern HANDLE sCon;
+extern LPCWSTR metroDX10Available;
+
+void Log(const wchar_t *fmt, ...);
+
+#if defined(SHOW_CONSOLE)
+static void SetupConsole();
+#endif
+
+bool IsDX10Available();
+bool GetDWORDRegKey(LPCWSTR name, DWORD &value);
+bool SetDWORDRegKey(LPCWSTR name, DWORD value);
diff --git a/browser/metro/shell/commandexecutehandler/CommandExecuteHandler.cpp b/browser/metro/shell/commandexecutehandler/CommandExecuteHandler.cpp
new file mode 100644
index 000000000000..0fa91ec7f2b3
--- /dev/null
+++ b/browser/metro/shell/commandexecutehandler/CommandExecuteHandler.cpp
@@ -0,0 +1,707 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+#include "CEHHelper.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#ifdef SHOW_CONSOLE
+#define DEBUG_DELAY_SHUTDOWN 1
+#endif
+
+// Heartbeat timer duration used while waiting for an incoming request.
+#define HEARTBEAT_MSEC 1000
+// Total number of heartbeats we wait before giving up and shutting down.
+#define REQUEST_WAIT_TIMEOUT 30
+// Pulled from desktop browser's shell
+#define APP_REG_NAME L"Firefox"
+
+static const WCHAR* kFirefoxExe = L"firefox.exe";
+static const WCHAR* kMetroFirefoxExe = L"firefox.exe";
+static const WCHAR* kDefaultMetroBrowserIDPathKey = L"FirefoxURL";
+static const WCHAR* kDemoMetroBrowserIDPathKey = L"Mozilla.Firefox.URL";
+
+template void SafeRelease(T **ppT)
+{
+ if (*ppT) {
+ (*ppT)->Release();
+ *ppT = NULL;
+ }
+}
+
+template HRESULT SetInterface(T **ppT, IUnknown *punk)
+{
+ SafeRelease(ppT);
+ return punk ? punk->QueryInterface(ppT) : E_NOINTERFACE;
+}
+
+class __declspec(uuid("5100FEC1-212B-4BF5-9BF8-3E650FD794A3"))
+ CExecuteCommandVerb : public IExecuteCommand,
+ public IObjectWithSelection,
+ public IInitializeCommand,
+ public IObjectWithSite,
+ public IExecuteCommandApplicationHostEnvironment
+{
+public:
+
+ CExecuteCommandVerb() :
+ mRef(1),
+ mShellItemArray(NULL),
+ mUnkSite(NULL),
+ mTargetIsFileSystemLink(false),
+ mIsDesktopRequest(true),
+ mRequestMet(false)
+ {
+ }
+
+ bool RequestMet() { return mRequestMet; }
+ long RefCount() { return mRef; }
+
+ // IUnknown
+ IFACEMETHODIMP QueryInterface(REFIID aRefID, void **aInt)
+ {
+ static const QITAB qit[] = {
+ QITABENT(CExecuteCommandVerb, IExecuteCommand),
+ QITABENT(CExecuteCommandVerb, IObjectWithSelection),
+ QITABENT(CExecuteCommandVerb, IInitializeCommand),
+ QITABENT(CExecuteCommandVerb, IObjectWithSite),
+ QITABENT(CExecuteCommandVerb, IExecuteCommandApplicationHostEnvironment),
+ { 0 },
+ };
+ return QISearch(this, qit, aRefID, aInt);
+ }
+
+ IFACEMETHODIMP_(ULONG) AddRef()
+ {
+ return InterlockedIncrement(&mRef);
+ }
+
+ IFACEMETHODIMP_(ULONG) Release()
+ {
+ long cRef = InterlockedDecrement(&mRef);
+ if (!cRef) {
+ delete this;
+ }
+ return cRef;
+ }
+
+ // IExecuteCommand
+ IFACEMETHODIMP SetKeyState(DWORD aKeyState)
+ {
+ mKeyState = aKeyState;
+ return S_OK;
+ }
+
+ IFACEMETHODIMP SetParameters(PCWSTR aParameters)
+ {
+ Log(L"SetParameters: '%s'", aParameters);
+ mParameters = aParameters;
+ return S_OK;
+ }
+
+ IFACEMETHODIMP SetPosition(POINT aPoint)
+ { return S_OK; }
+
+ IFACEMETHODIMP SetShowWindow(int aShowFlag)
+ { return S_OK; }
+
+ IFACEMETHODIMP SetNoShowUI(BOOL aNoUI)
+ { return S_OK; }
+
+ IFACEMETHODIMP SetDirectory(PCWSTR aDirPath)
+ { return S_OK; }
+
+ IFACEMETHODIMP Execute();
+
+ // IObjectWithSelection
+ IFACEMETHODIMP SetSelection(IShellItemArray *aArray)
+ {
+ if (!aArray) {
+ return E_FAIL;
+ }
+
+ SetInterface(&mShellItemArray, aArray);
+
+ DWORD count = 0;
+ aArray->GetCount(&count);
+ if (!count) {
+ return E_FAIL;
+ }
+
+#ifdef SHOW_CONSOLE
+ Log(L"SetSelection param count: %d", count);
+ for (int idx = 0; idx < count; idx++) {
+ IShellItem* item = NULL;
+ if (SUCCEEDED(aArray->GetItemAt(idx, &item))) {
+ LPWSTR str = NULL;
+ if (FAILED(item->GetDisplayName(SIGDN_FILESYSPATH, &str))) {
+ if (FAILED(item->GetDisplayName(SIGDN_URL, &str))) {
+ Log(L"Failed to get a shell item array item.");
+ item->Release();
+ continue;
+ }
+ }
+ item->Release();
+ Log(L"SetSelection param: '%s'", str);
+ CoTaskMemFree(str);
+ }
+ }
+#endif
+
+ IShellItem* item = NULL;
+ if (FAILED(aArray->GetItemAt(0, &item))) {
+ return E_FAIL;
+ }
+
+ bool isFileSystem = false;
+ if (!SetTargetPath(item) || !mTarget.GetLength()) {
+ Log(L"SetTargetPath failed.");
+ return E_FAIL;
+ }
+ item->Release();
+
+ Log(L"SetSelection target: %s", mTarget);
+ return S_OK;
+ }
+
+ IFACEMETHODIMP GetSelection(REFIID aRefID, void **aInt)
+ {
+ *aInt = NULL;
+ return mShellItemArray ? mShellItemArray->QueryInterface(aRefID, aInt) : E_FAIL;
+ }
+
+ // IInitializeCommand
+ IFACEMETHODIMP Initialize(PCWSTR aVerb, IPropertyBag* aPropBag)
+ {
+ if (!aVerb)
+ return E_FAIL;
+ // 'open', 'edit', etc. Based on our registry settings
+ Log(L"Initialize(%s)", aVerb);
+ mVerb = aVerb;
+ return S_OK;
+ }
+
+ // IObjectWithSite
+ IFACEMETHODIMP SetSite(IUnknown *aUnkSite)
+ {
+ SetInterface(&mUnkSite, aUnkSite);
+ return S_OK;
+ }
+
+ IFACEMETHODIMP GetSite(REFIID aRefID, void **aInt)
+ {
+ *aInt = NULL;
+ return mUnkSite ? mUnkSite->QueryInterface(aRefID, aInt) : E_FAIL;
+ }
+
+ // IExecuteCommandApplicationHostEnvironment
+ IFACEMETHODIMP GetValue(AHE_TYPE *aLaunchType)
+ {
+ Log(L"IExecuteCommandApplicationHostEnvironment::GetValue()");
+ *aLaunchType = AHE_DESKTOP;
+ mIsDesktopRequest = true;
+
+ if (!mUnkSite) {
+ Log(L"No mUnkSite.");
+ return S_OK;
+ }
+
+ HRESULT hr;
+ IServiceProvider* pSvcProvider = NULL;
+ hr = mUnkSite->QueryInterface(IID_IServiceProvider, (void**)&pSvcProvider);
+ if (!pSvcProvider) {
+ Log(L"Couldn't get IServiceProvider service from explorer. (%X)", hr);
+ return S_OK;
+ }
+
+ IExecuteCommandHost* pHost = NULL;
+ // If we can't get this it's a conventional desktop launch
+ hr = pSvcProvider->QueryService(SID_ExecuteCommandHost,
+ IID_IExecuteCommandHost, (void**)&pHost);
+ if (!pHost) {
+ Log(L"Couldn't get IExecuteCommandHost service from explorer. (%X)", hr);
+ SafeRelease(&pSvcProvider);
+ return S_OK;
+ }
+ SafeRelease(&pSvcProvider);
+
+ EC_HOST_UI_MODE mode;
+ if (FAILED(pHost->GetUIMode(&mode))) {
+ Log(L"GetUIMode failed.");
+ SafeRelease(&pHost);
+ return S_OK;
+ }
+
+ // 0 - launched from desktop
+ // 1 - ?
+ // 2 - launched from tile interface
+ Log(L"GetUIMode: %d", mode);
+
+ if (!IsDefaultBrowser()) {
+ mode = ECHUIM_DESKTOP;
+ }
+
+ if (mode == ECHUIM_DESKTOP) {
+ Log(L"returning AHE_DESKTOP");
+ SafeRelease(&pHost);
+ return S_OK;
+ }
+ SafeRelease(&pHost);
+
+ if (!IsDX10Available()) {
+ Log(L"returning AHE_DESKTOP because DX10 is not available");
+ *aLaunchType = AHE_DESKTOP;
+ mIsDesktopRequest = true;
+ } else {
+ Log(L"returning AHE_IMMERSIVE");
+ *aLaunchType = AHE_IMMERSIVE;
+ mIsDesktopRequest = false;
+ }
+ return S_OK;
+ }
+
+ bool IsDefaultBrowser()
+ {
+ bool result = false;
+ IApplicationAssociationRegistration* pAAR;
+ HRESULT hr = CoCreateInstance(CLSID_ApplicationAssociationRegistration,
+ NULL,
+ CLSCTX_INPROC,
+ IID_IApplicationAssociationRegistration,
+ (void**)&pAAR);
+ if (SUCCEEDED(hr)) {
+ BOOL res;
+ hr = pAAR->QueryAppIsDefaultAll(AL_EFFECTIVE,
+ APP_REG_NAME,
+ &res);
+ Log(L"QueryAppIsDefaultAll: %d", res);
+ if (!res)
+ return false;
+ // Make sure the Prog ID matches what we have
+ LPWSTR registeredApp;
+ hr = pAAR->QueryCurrentDefault(L"http", AT_URLPROTOCOL, AL_EFFECTIVE,
+ ®isteredApp);
+ Log(L"QueryCurrentDefault: %X", hr);
+ if (SUCCEEDED(hr)) {
+ Log(L"registeredApp=%s", registeredApp);
+ result = !wcsicmp(registeredApp, kDefaultMetroBrowserIDPathKey);
+ if (!result) {
+ result = !wcsicmp(registeredApp, kDemoMetroBrowserIDPathKey);
+ }
+ CoTaskMemFree(registeredApp);
+ } else {
+ result = false;
+ }
+
+ pAAR->Release();
+ return result;
+ }
+ return result;
+ }
+private:
+ ~CExecuteCommandVerb()
+ {
+ SafeRelease(&mShellItemArray);
+ SafeRelease(&mUnkSite);
+ }
+
+ void LaunchDesktopBrowser();
+ bool SetTargetPath(IShellItem* aItem);
+ bool IsTargetBrowser();
+
+ long mRef;
+ IShellItemArray *mShellItemArray;
+ IUnknown *mUnkSite;
+ CStringW mVerb;
+ CStringW mTarget;
+ CStringW mParameters;
+ bool mTargetIsFileSystemLink;
+ DWORD mKeyState;
+ bool mIsDesktopRequest;
+ bool mRequestMet;
+};
+
+/*
+ * Retrieve our module dir path.
+ *
+ * @aPathBuffer Buffer to fill
+ */
+static bool GetModulePath(CStringW& aPathBuffer)
+{
+ WCHAR buffer[MAX_PATH];
+ memset(buffer, 0, sizeof(buffer));
+
+ if (!GetModuleFileName(NULL, buffer, MAX_PATH)) {
+ Log(L"GetModuleFileName failed.");
+ return false;
+ }
+
+ WCHAR* slash = wcsrchr(buffer, '\\');
+ if (!slash)
+ return false;
+ *slash = '\0';
+
+ aPathBuffer = buffer;
+ return true;
+}
+
+/*
+ * Retrieve 'module dir path\firefox.exe'
+ *
+ * @aPathBuffer Buffer to fill
+ */
+static bool GetDesktopBrowserPath(CStringW& aPathBuffer)
+{
+ if (!GetModulePath(aPathBuffer))
+ return false;
+
+ // ceh.exe sits in dist/bin root with the desktop browser. Since this
+ // is a firefox only component, this hardcoded filename is ok.
+ aPathBuffer.Append(L"\\");
+ aPathBuffer.Append(kFirefoxExe);
+ return true;
+}
+
+/*
+ * Retrieve the app model id of the firefox metro browser.
+ *
+ * @aPathBuffer Buffer to fill
+ * @aCharLength Length of buffer to fill in characters
+ */
+static bool GetDefaultBrowserAppModelID(WCHAR* aIDBuffer,
+ long aCharLength)
+{
+ if (!aIDBuffer || aCharLength <= 0)
+ return false;
+
+ memset(aIDBuffer, 0, (sizeof(WCHAR)*aCharLength));
+
+ HKEY key;
+ if (RegOpenKeyExW(HKEY_CLASSES_ROOT, kDefaultMetroBrowserIDPathKey,
+ 0, KEY_READ, &key) != ERROR_SUCCESS) {
+ return false;
+ }
+ DWORD len = aCharLength * sizeof(WCHAR);
+ memset(aIDBuffer, 0, len);
+ if (RegQueryValueExW(key, L"AppUserModelID", NULL, NULL,
+ (LPBYTE)aIDBuffer, &len) != ERROR_SUCCESS || !len) {
+ RegCloseKey(key);
+ return false;
+ }
+ RegCloseKey(key);
+ return true;
+}
+
+/*
+ * Determines if the current target points directly to a particular
+ * browser or to a file or url.
+ */
+bool CExecuteCommandVerb::IsTargetBrowser()
+{
+ if (!mTarget.GetLength() || !mTargetIsFileSystemLink)
+ return false;
+
+ CStringW modulePath;
+ if (!GetModulePath(modulePath))
+ return false;
+
+ modulePath.MakeLower();
+
+ CStringW tmpTarget = mTarget;
+ tmpTarget.Replace(L"\"", L"");
+ tmpTarget.MakeLower();
+
+ CStringW checkPath;
+
+ checkPath = modulePath;
+ checkPath.Append(L"\\");
+ checkPath.Append(kFirefoxExe);
+ if (tmpTarget == checkPath) {
+ return true;
+ }
+ return false;
+}
+
+/*
+ * Updates the current target based on the contents of
+ * a shell item.
+ */
+bool CExecuteCommandVerb::SetTargetPath(IShellItem* aItem)
+{
+ if (!aItem)
+ return false;
+
+ LPWSTR str = NULL;
+ mTargetIsFileSystemLink = true;
+ if (FAILED(aItem->GetDisplayName(SIGDN_FILESYSPATH, &str))) {
+ if (FAILED(aItem->GetDisplayName(SIGDN_URL, &str))) {
+ return false;
+ }
+ mTargetIsFileSystemLink = false;
+ }
+
+ mTarget = str;
+ CoTaskMemFree(str);
+ return true;
+}
+
+/*
+ * Desktop launch - Launch the destop browser to display the current
+ * target using shellexecute.
+ */
+void CExecuteCommandVerb::LaunchDesktopBrowser()
+{
+ CStringW browserPath;
+ if (!GetDesktopBrowserPath(browserPath)) {
+ return;
+ }
+
+ // If a taskbar shortcut, link or local file is clicked, the target will
+ // be the browser exe or file.
+ CStringW params;
+ if (!IsTargetBrowser()) {
+ params += "-url ";
+ params += mTarget;
+ }
+
+ Log(L"Desktop Launch: verb:%s exe:%s params:%s", mVerb, browserPath, params);
+
+ SHELLEXECUTEINFOW seinfo;
+ memset(&seinfo, 0, sizeof(seinfo));
+ seinfo.cbSize = sizeof(SHELLEXECUTEINFOW);
+ seinfo.fMask = NULL;
+ seinfo.hwnd = NULL;
+ seinfo.lpVerb = NULL;
+ seinfo.lpFile = browserPath;
+ seinfo.lpParameters = params;
+ seinfo.lpDirectory = NULL;
+ seinfo.nShow = SW_SHOWNORMAL;
+
+ ShellExecuteExW(&seinfo);
+}
+
+class AutoSetRequestMet
+{
+public:
+ explicit AutoSetRequestMet(bool* aFlag) :
+ mFlag(aFlag) {}
+ ~AutoSetRequestMet() { if (mFlag) *mFlag = true; }
+private:
+ bool* mFlag;
+};
+
+IFACEMETHODIMP CExecuteCommandVerb::Execute()
+{
+ Log(L"Execute()");
+
+ // We shut down when this flips to true
+ AutoSetRequestMet asrm(&mRequestMet);
+
+ if (!mTarget.GetLength()) {
+ return E_FAIL;
+ }
+
+ // Launch on the desktop
+ if (mIsDesktopRequest) {
+ LaunchDesktopBrowser();
+ return S_OK;
+ }
+
+ // Launch into Metro
+ IApplicationActivationManager* activateMgr = NULL;
+ DWORD processID;
+ if (FAILED(CoCreateInstance(CLSID_ApplicationActivationManager, NULL,
+ CLSCTX_LOCAL_SERVER,
+ IID_IApplicationActivationManager,
+ (void**)&activateMgr))) {
+ Log(L"CoCreateInstance failed, launching on desktop.");
+ LaunchDesktopBrowser();
+ return S_OK;
+ }
+
+ HRESULT hr;
+ WCHAR appModelID[256];
+ if (!GetDefaultBrowserAppModelID(appModelID, (sizeof(appModelID)/sizeof(WCHAR)))) {
+ Log(L"GetDefaultBrowserAppModelID failed, launching on desktop.");
+ activateMgr->Release();
+ LaunchDesktopBrowser();
+ return S_OK;
+ }
+
+ // Hand off focus rights to the out-of-process activation server. Without
+ // this the metro interface won't launch.
+ hr = CoAllowSetForegroundWindow(activateMgr, NULL);
+ if (FAILED(hr)) {
+ Log(L"CoAllowSetForegroundWindow result %X", hr);
+ activateMgr->Release();
+ return false;
+ }
+
+ Log(L"Metro Launch: verb:%s appid:%s params:%s", mVerb, appModelID, mTarget);
+
+ // shortcuts to the application
+ if (IsTargetBrowser()) {
+ hr = activateMgr->ActivateApplication(appModelID, L"", AO_NONE, &processID);
+ Log(L"ActivateApplication result %X", hr);
+ // files
+ } else if (mTargetIsFileSystemLink) {
+ hr = activateMgr->ActivateForFile(appModelID, mShellItemArray, mVerb, &processID);
+ Log(L"ActivateForFile result %X", hr);
+ // protocols
+ } else {
+ hr = activateMgr->ActivateForProtocol(appModelID, mShellItemArray, &processID);
+ Log(L"ActivateForProtocol result %X", hr);
+ }
+ activateMgr->Release();
+ return S_OK;
+}
+
+class ClassFactory : public IClassFactory
+{
+public:
+ ClassFactory(IUnknown *punkObject);
+ ~ClassFactory();
+ STDMETHODIMP Register(CLSCTX classContent, REGCLS classUse);
+ STDMETHODIMP QueryInterface(REFIID riid, void **ppv);
+ STDMETHODIMP_(ULONG) AddRef() { return 2; }
+ STDMETHODIMP_(ULONG) Release() { return 1; }
+ STDMETHODIMP CreateInstance(IUnknown *punkOuter, REFIID riid, void **ppv);
+ STDMETHODIMP LockServer(BOOL);
+private:
+ IUnknown* mUnkObject;
+ DWORD mRegID;
+};
+
+ClassFactory::ClassFactory(IUnknown* aUnkObj) :
+ mUnkObject(aUnkObj),
+ mRegID(0)
+{
+ if (mUnkObject) {
+ mUnkObject->AddRef();
+ }
+}
+
+ClassFactory::~ClassFactory()
+{
+ if (mRegID) {
+ CoRevokeClassObject(mRegID);
+ }
+ mUnkObject->Release();
+}
+
+STDMETHODIMP
+ClassFactory::Register(CLSCTX aClass, REGCLS aUse)
+{
+ return CoRegisterClassObject(__uuidof(CExecuteCommandVerb),
+ static_cast(this),
+ aClass, aUse, &mRegID);
+}
+
+STDMETHODIMP
+ClassFactory::QueryInterface(REFIID riid, void **ppv)
+{
+ IUnknown *punk = NULL;
+ if (riid == IID_IUnknown || riid == IID_IClassFactory) {
+ punk = static_cast(this);
+ }
+ *ppv = punk;
+ if (punk) {
+ punk->AddRef();
+ return S_OK;
+ } else {
+ return E_NOINTERFACE;
+ }
+}
+
+STDMETHODIMP
+ClassFactory::CreateInstance(IUnknown *punkOuter, REFIID riid, void **ppv)
+{
+ *ppv = NULL;
+ if (punkOuter)
+ return CLASS_E_NOAGGREGATION;
+ return mUnkObject->QueryInterface(riid, ppv);
+}
+
+LONG gObjRefCnt;
+
+STDMETHODIMP
+ClassFactory::LockServer(BOOL fLock)
+{
+ if (fLock)
+ InterlockedIncrement(&gObjRefCnt);
+ else
+ InterlockedDecrement(&gObjRefCnt);
+ Log(L"ClassFactory::LockServer() %d", gObjRefCnt);
+ return S_OK;
+}
+
+int APIENTRY wWinMain(HINSTANCE, HINSTANCE, PWSTR pszCmdLine, int)
+{
+#if defined(SHOW_CONSOLE)
+ SetupConsole();
+#endif
+ //Log(pszCmdLine);
+
+ if (!wcslen(pszCmdLine) || StrStrI(pszCmdLine, L"-Embedding"))
+ {
+ CoInitialize(NULL);
+
+ CExecuteCommandVerb *pHandler = new CExecuteCommandVerb();
+ if (!pHandler)
+ return E_OUTOFMEMORY;
+
+ IUnknown* ppi;
+ pHandler->QueryInterface(IID_IUnknown, (void**)&ppi);
+ if (!ppi)
+ return E_FAIL;
+
+ ClassFactory classFactory(ppi);
+ ppi->Release();
+ ppi = NULL;
+
+ // REGCLS_SINGLEUSE insures we only get used once and then discarded.
+ if (FAILED(classFactory.Register(CLSCTX_LOCAL_SERVER, REGCLS_SINGLEUSE)))
+ return -1;
+
+ if (!SetTimer(NULL, 1, HEARTBEAT_MSEC, NULL)) {
+ Log(L"Failed to set timer, can't process request.");
+ return -1;
+ }
+
+ MSG msg;
+ long beatCount = 0;
+ while (GetMessage(&msg, 0, 0, 0) > 0) {
+ if (msg.message == WM_TIMER) {
+ if (++beatCount > REQUEST_WAIT_TIMEOUT ||
+ (pHandler->RequestMet() && pHandler->RefCount() < 2)) {
+ break;
+ }
+ }
+ TranslateMessage(&msg);
+ DispatchMessage(&msg);
+ }
+
+#ifdef DEBUG_DELAY_SHUTDOWN
+ Sleep(10000);
+#endif
+ CoUninitialize();
+ return 0;
+ }
+ return 0;
+}
diff --git a/browser/metro/shell/commandexecutehandler/CommandExecuteHandler.def b/browser/metro/shell/commandexecutehandler/CommandExecuteHandler.def
new file mode 100644
index 000000000000..5adbfef6fb1b
--- /dev/null
+++ b/browser/metro/shell/commandexecutehandler/CommandExecuteHandler.def
@@ -0,0 +1,6 @@
+LIBRARY CommandExecuteHandler.dll
+
+EXPORTS
+ DllCanUnloadNow PRIVATE
+ DllGetClassObject PRIVATE
+
diff --git a/browser/metro/shell/commandexecutehandler/Makefile.in b/browser/metro/shell/commandexecutehandler/Makefile.in
new file mode 100644
index 000000000000..a93523523a7f
--- /dev/null
+++ b/browser/metro/shell/commandexecutehandler/Makefile.in
@@ -0,0 +1,42 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEPTH = ../../../..
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+NO_PROFILE_GUIDED_OPTIMIZE = 1
+
+include $(topsrcdir)/config/config.mk
+
+# We want this exe in dist/bin
+DIST_SUBDIR =
+
+PROGRAM = CommandExecuteHandler$(BIN_SUFFIX)
+DIST_PROGRAM = CommandExecuteHandler$(BIN_SUFFIX)
+
+# Don't link against mozglue.dll
+MOZ_GLUE_LDFLAGS =
+MOZ_GLUE_PROGRAM_LDFLAGS =
+
+CPPSRCS = \
+ CommandExecuteHandler.cpp \
+ CEHHelper.cpp \
+ $(NULL)
+
+OS_LIBS = \
+ kernel32.lib \
+ user32.lib \
+ ole32.lib \
+ shlwapi.lib \
+ propsys.lib \
+ advapi32.lib \
+ $(NULL)
+
+DEFINES += -DUNICODE -D_UNICODE -DNS_NO_XPCOM
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/metro/shell/linktool/Makefile.in b/browser/metro/shell/linktool/Makefile.in
new file mode 100644
index 000000000000..56bee83fd975
--- /dev/null
+++ b/browser/metro/shell/linktool/Makefile.in
@@ -0,0 +1,33 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEPTH = ../../../..
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+NO_PROFILE_GUIDED_OPTIMIZE = 1
+
+include $(topsrcdir)/config/config.mk
+
+DIST_SUBDIR = metro/install
+
+PROGRAM = linktool$(BIN_SUFFIX)
+
+CPPSRCS = linktool.cpp
+
+OS_LIBS = \
+ kernel32.lib \
+ user32.lib \
+ ole32.lib \
+ shlwapi.lib \
+ shell32.lib \
+ propsys.lib \
+ $(NULL)
+
+DEFINES += -DUNICODE -D_UNICODE
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/metro/shell/linktool/linktool.cpp b/browser/metro/shell/linktool/linktool.cpp
new file mode 100644
index 000000000000..2fba6bf298af
--- /dev/null
+++ b/browser/metro/shell/linktool/linktool.cpp
@@ -0,0 +1,286 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#undef WINVER
+#undef _WIN32_WINNT
+#define WINVER 0x602
+#define _WIN32_WINNT 0x602
+
+#include
+#include
+#include
+#include
+#include
+#define INITGUID
+#include
+#include
+#include
+
+// Indicates that an application supports dual desktop and immersive modes. In Windows 8, this property is only applicable for web browsers.
+//DEFINE_PROPERTYKEY(PKEY_AppUserModel_IsDualMode, 0x9F4C2855, 0x9F79, 0x4B39, 0xA8, 0xD0, 0xE1, 0xD4, 0x2D, 0xE1, 0xD5, 0xF3, 11);
+
+void DumpParameters(LPCWSTR aTargetPath, LPCWSTR aShortcutPath, LPCWSTR aAppModelID, LPCWSTR aDescription)
+{
+ if (aTargetPath)
+ wprintf(L"target path: '%s'\n", aTargetPath);
+ if (aShortcutPath)
+ wprintf(L"shortcut path: '%s'\n", aShortcutPath);
+ if (aAppModelID)
+ wprintf(L"app id: '%s'\n", aAppModelID);
+ if (aDescription)
+ wprintf(L"description: '%s'\n", aDescription);
+}
+
+HRESULT
+SetShortcutProps(LPCWSTR aShortcutPath, LPCWSTR aAppModelID, bool aSetID, bool aSetMode)
+{
+ HRESULT hres;
+ ::CoInitialize(NULL);
+
+ IPropertyStore *m_pps = NULL;
+ if (FAILED(hres = SHGetPropertyStoreFromParsingName(aShortcutPath, NULL, GPS_READWRITE, IID_PPV_ARGS(&m_pps)))) {
+ printf("SHGetPropertyStoreFromParsingName failed\n");
+ goto Exit;
+ }
+
+ if (aSetMode) {
+ PROPVARIANT propvar;
+ if (FAILED(hres = InitPropVariantFromBoolean(true, &propvar)) ||
+ FAILED(hres = m_pps->SetValue(PKEY_AppUserModel_IsDualMode, propvar))) {
+ goto Exit;
+ }
+ PropVariantClear(&propvar);
+ }
+
+ if (aSetID && aAppModelID) {
+ PROPVARIANT propvar;
+ if (FAILED(hres = InitPropVariantFromString(aAppModelID, &propvar)) ||
+ FAILED(hres = m_pps->SetValue(PKEY_AppUserModel_ID, propvar))) {
+ goto Exit;
+ }
+ PropVariantClear(&propvar);
+ }
+
+ hres = m_pps->Commit();
+
+ Exit:
+
+ if (m_pps) {
+ m_pps->Release();
+ }
+
+ CoUninitialize();
+ return hres;
+}
+
+HRESULT
+PrintShortcutProps(LPCWSTR aTargetPath)
+{
+ HRESULT hres;
+ ::CoInitialize(NULL);
+
+ IPropertyStore *m_pps = NULL;
+ if (FAILED(hres = SHGetPropertyStoreFromParsingName(aTargetPath, NULL, GPS_READWRITE, IID_PPV_ARGS(&m_pps)))) {
+ printf("SHGetPropertyStoreFromParsingName failed\n");
+ goto Exit;
+ }
+
+ bool found = false;
+
+ PROPVARIANT propvar;
+ if (SUCCEEDED(hres = m_pps->GetValue(PKEY_AppUserModel_IsDualMode, &propvar)) && propvar.vt == VT_BOOL && propvar.boolVal == -1) {
+ printf("PKEY_AppUserModel_IsDualMode found\n");
+ PropVariantClear(&propvar);
+ found = true;
+ }
+
+ if (SUCCEEDED(hres = m_pps->GetValue(PKEY_AppUserModel_ID, &propvar)) && propvar.pwszVal) {
+ printf("PKEY_AppUserModel_ID found ");
+ wprintf(L"value: '%s'\n", propvar.pwszVal);
+ PropVariantClear(&propvar);
+ found = true;
+ }
+
+ if (!found) {
+ printf("no known properties found.\n");
+ }
+
+ Exit:
+
+ if (m_pps) {
+ m_pps->Release();
+ }
+
+ CoUninitialize();
+ return hres;
+}
+
+HRESULT
+CreateLink(LPCWSTR aTargetPath, LPCWSTR aShortcutPath, LPCWSTR aDescription)
+{
+ HRESULT hres;
+ IShellLink* psl;
+
+ wprintf(L"creating shortcut: '%s'\n", aShortcutPath);
+
+ CoInitialize(NULL);
+
+ hres = CoCreateInstance(CLSID_ShellLink, NULL, CLSCTX_INPROC_SERVER,
+ IID_IShellLink, (LPVOID*)&psl);
+ if (FAILED(hres)) {
+ CoUninitialize();
+ return hres;
+ }
+ psl->SetPath(aTargetPath);
+ if (aDescription) {
+ psl->SetDescription(aDescription);
+ } else {
+ psl->SetDescription(L"");
+ }
+
+ IPersistFile* ppf = NULL;
+ hres = psl->QueryInterface(IID_IPersistFile, (LPVOID*)&ppf);
+
+ if (SUCCEEDED(hres)) {
+ hres = ppf->Save(aShortcutPath, TRUE);
+ ppf->Release();
+ }
+ psl->Release();
+ CoUninitialize();
+ return hres;
+}
+
+void DumpCommands()
+{
+ printf("control options:\n");
+ printf(" /CREATE create a shortcut for the target file.\n");
+ printf(" /UPDATE update properties on the target file.\n");
+ printf(" /PRINT print the known properties set on the target file.\n");
+ printf("parameters:\n");
+ printf(" /T(path) the full path and filename of the target file.\n");
+ printf(" /S(path) with CREATE, the full path and filename of the shortcut to create.\n");
+ printf(" /D(string) with CREATE, adds a description to the shortcut.\n");
+ printf(" /A(id) the app model id to assign to the shortcut or target file.\n");
+ printf(" /M enable support for dual desktop and immersive modes on the shortcut or target file.\n");
+}
+
+int wmain(int argc, WCHAR* argv[])
+{
+ WCHAR shortcutPathStr[MAX_PATH];
+ WCHAR targetPathStr[MAX_PATH];
+ WCHAR appModelIDStr[MAX_PATH];
+ WCHAR descriptionStr[MAX_PATH];
+
+ shortcutPathStr[0] = '\0';
+ targetPathStr[0] = '\0';
+ appModelIDStr[0] = '\0';
+ descriptionStr[0] = '\0';
+
+ bool createShortcutFound = false;
+ bool updateFound = false;
+ bool shortcutPathFound = false;
+ bool targetPathFound = false;
+ bool appModelIDFound = false;
+ bool modeFound = false;
+ bool descriptionFound = false;
+ bool printFound = false;
+
+ int idx;
+ for (idx = 1; idx < argc; idx++) {
+ if (!wcscmp(L"/CREATE", argv[idx])) {
+ createShortcutFound = true;
+ continue;
+ }
+ if (!wcscmp(L"/UPDATE", argv[idx])) {
+ updateFound = true;
+ continue;
+ }
+ if (!wcscmp(L"/PRINT", argv[idx])) {
+ printFound = true;
+ continue;
+ }
+
+ if (!wcsncmp(L"/S", argv[idx], 2) && wcslen(argv[idx]) > 2) {
+ wcscpy_s(shortcutPathStr, MAX_PATH, (argv[idx]+2));
+ shortcutPathFound = true;
+ continue;
+ }
+ if (!wcsncmp(L"/T", argv[idx], 2) && wcslen(argv[idx]) > 2) {
+ wcscpy_s(targetPathStr, MAX_PATH, (argv[idx]+2));
+ targetPathFound = true;
+ continue;
+ }
+ if (!wcsncmp(L"/A", argv[idx], 2) && wcslen(argv[idx]) > 2) {
+ wcscpy_s(appModelIDStr, MAX_PATH, (argv[idx]+2));
+ appModelIDFound = true;
+ continue;
+ }
+ if (!wcsncmp(L"/D", argv[idx], 2) && wcslen(argv[idx]) > 2 && wcslen(argv[idx]) < MAX_PATH) {
+ wcscpy_s(descriptionStr, MAX_PATH, (argv[idx]+2));
+ descriptionFound = true;
+ continue;
+ }
+ if (!wcscmp(L"/M", argv[idx])) {
+ modeFound = true;
+ continue;
+ }
+ }
+
+ DumpParameters(targetPathStr, shortcutPathStr, appModelIDStr, descriptionStr);
+
+ if (!createShortcutFound && !updateFound && !printFound) {
+ DumpCommands();
+ return 0;
+ }
+
+ if (!targetPathFound) {
+ printf("missing target file path.\n");
+ return -1;
+ }
+
+ HRESULT hres;
+
+ if (printFound) {
+ if (FAILED(hres = PrintShortcutProps(targetPathStr))) {
+ printf("failed printing target props HRESULT=%X\n", hres);
+ return -1;
+ }
+ return 0;
+ }
+
+ if (createShortcutFound && !shortcutPathFound) {
+ printf("missing shortcut file path.\n");
+ return -1;
+ }
+
+ if (updateFound && !appModelIDFound && !modeFound) {
+ printf("no properties selected.\n");
+ return -1;
+ }
+
+ if (createShortcutFound) {
+ if (FAILED(hres = CreateLink(targetPathStr, shortcutPathStr, (descriptionFound ? descriptionStr : NULL)))) {
+ printf("failed creating shortcut HRESULT=%X\n", hres);
+ return -1;
+ }
+ }
+
+ LPCWSTR target;
+ if (createShortcutFound) {
+ target = shortcutPathStr;
+ } else {
+ target = targetPathStr;
+ }
+
+ if (appModelIDFound || modeFound) {
+ if (FAILED(hres = SetShortcutProps(target, (appModelIDFound ? appModelIDStr : NULL), appModelIDFound, modeFound))) {
+ printf("failed adding property HRESULT=%X\n", hres);
+ return -1;
+ }
+ }
+
+ return 0;
+}
+
diff --git a/browser/metro/shell/priconfig.xml b/browser/metro/shell/priconfig.xml
new file mode 100644
index 000000000000..b2bed133b555
--- /dev/null
+++ b/browser/metro/shell/priconfig.xml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/browser/metro/shell/resources.pri b/browser/metro/shell/resources.pri
new file mode 100644
index 000000000000..c57e50a7877e
Binary files /dev/null and b/browser/metro/shell/resources.pri differ
diff --git a/browser/metro/shell/testing/Makefile.in b/browser/metro/shell/testing/Makefile.in
new file mode 100644
index 000000000000..39d75bc18ede
--- /dev/null
+++ b/browser/metro/shell/testing/Makefile.in
@@ -0,0 +1,35 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEPTH = ../../../..
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+include $(topsrcdir)/config/config.mk
+
+NO_PROFILE_GUIDED_OPTIMIZE = 1
+
+# We want this exe in dist/bin
+DIST_SUBDIR =
+
+PROGRAM = metrotestharness$(BIN_SUFFIX)
+
+CPPSRCS = \
+ metrotestharness.cpp \
+ $(NULL)
+
+OS_LIBS = \
+ kernel32.lib \
+ user32.lib \
+ ole32.lib \
+ shlwapi.lib \
+ propsys.lib \
+ advapi32.lib \
+ $(NULL)
+
+DEFINES += -DUNICODE -D_UNICODE
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/metro/shell/testing/metrotestharness.cpp b/browser/metro/shell/testing/metrotestharness.cpp
new file mode 100644
index 000000000000..953fc2e1c77e
--- /dev/null
+++ b/browser/metro/shell/testing/metrotestharness.cpp
@@ -0,0 +1,260 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#undef WINVER
+#undef _WIN32_WINNT
+#define WINVER 0x602
+#define _WIN32_WINNT 0x602
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+static const WCHAR* kFirefoxExe = L"firefox.exe";
+static const WCHAR* kDefaultMetroBrowserIDPathKey = L"FirefoxURL";
+static const WCHAR* kDemoMetroBrowserIDPathKey = L"Mozilla.Firefox.URL";
+
+static void Log(const wchar_t *fmt, ...)
+{
+ va_list a = NULL;
+ wchar_t szDebugString[1024];
+ if(!lstrlenW(fmt))
+ return;
+ va_start(a,fmt);
+ vswprintf(szDebugString, 1024, fmt, a);
+ va_end(a);
+ if(!lstrlenW(szDebugString))
+ return;
+
+ wprintf(L"INFO | metrotestharness.exe | %s\n", szDebugString);
+ fflush(stdout);
+}
+
+static void Fail(const wchar_t *fmt, ...)
+{
+ va_list a = NULL;
+ wchar_t szDebugString[1024];
+ if(!lstrlenW(fmt))
+ return;
+ va_start(a,fmt);
+ vswprintf(szDebugString, 1024, fmt, a);
+ va_end(a);
+ if(!lstrlenW(szDebugString))
+ return;
+
+ wprintf(L"TEST-UNEXPECTED-FAIL | metrotestharness.exe | %s\n", szDebugString);
+ fflush(stdout);
+}
+
+/*
+ * Retrieve our module dir path.
+ *
+ * @aPathBuffer Buffer to fill
+ */
+static bool GetModulePath(CStringW& aPathBuffer)
+{
+ WCHAR buffer[MAX_PATH];
+ memset(buffer, 0, sizeof(buffer));
+
+ if (!GetModuleFileName(NULL, buffer, MAX_PATH)) {
+ Fail(L"GetModuleFileName failed.");
+ return false;
+ }
+
+ WCHAR* slash = wcsrchr(buffer, '\\');
+ if (!slash)
+ return false;
+ *slash = '\0';
+
+ aPathBuffer = buffer;
+ return true;
+}
+
+/*
+ * Retrieve 'module dir path\firefox.exe'
+ *
+ * @aPathBuffer Buffer to fill
+ */
+static bool GetDesktopBrowserPath(CStringW& aPathBuffer)
+{
+ if (!GetModulePath(aPathBuffer))
+ return false;
+
+ // ceh.exe sits in dist/bin root with the desktop browser. Since this
+ // is a firefox only component, this hardcoded filename is ok.
+ aPathBuffer.Append(L"\\");
+ aPathBuffer.Append(kFirefoxExe);
+ return true;
+}
+
+/*
+ * Retrieve the app model id of the firefox metro browser.
+ *
+ * @aPathBuffer Buffer to fill
+ * @aCharLength Length of buffer to fill in characters
+ */
+static bool GetDefaultBrowserAppModelID(WCHAR* aIDBuffer,
+ long aCharLength)
+{
+ if (!aIDBuffer || aCharLength <= 0)
+ return false;
+
+ memset(aIDBuffer, 0, (sizeof(WCHAR)*aCharLength));
+
+ HKEY key;
+ if (RegOpenKeyExW(HKEY_CLASSES_ROOT, kDefaultMetroBrowserIDPathKey,
+ 0, KEY_READ, &key) != ERROR_SUCCESS) {
+ if (RegOpenKeyExW(HKEY_CLASSES_ROOT, kDemoMetroBrowserIDPathKey,
+ 0, KEY_READ, &key) != ERROR_SUCCESS) {
+ return false;
+ }
+ }
+ DWORD len = aCharLength * sizeof(WCHAR);
+ memset(aIDBuffer, 0, len);
+ if (RegQueryValueExW(key, L"AppUserModelID", NULL, NULL,
+ (LPBYTE)aIDBuffer, &len) != ERROR_SUCCESS || !len) {
+ RegCloseKey(key);
+ return false;
+ }
+ RegCloseKey(key);
+ return true;
+}
+
+CString sAppParams;
+
+static bool Launch()
+{
+ Log(L"Launching browser...");
+
+ DWORD processID;
+
+ // The interface that allows us to activate the browser
+ IApplicationActivationManager* activateMgr = NULL;
+ if (FAILED(CoCreateInstance(CLSID_ApplicationActivationManager, NULL,
+ CLSCTX_LOCAL_SERVER,
+ IID_IApplicationActivationManager,
+ (void**)&activateMgr))) {
+ Fail(L"CoCreateInstance CLSID_ApplicationActivationManager failed.");
+ return false;
+ }
+
+ HRESULT hr;
+ WCHAR appModelID[256];
+ // Activation is based on the browser's registered app model id
+ if (!GetDefaultBrowserAppModelID(appModelID, (sizeof(appModelID)/sizeof(WCHAR)))) {
+ Fail(L"GetDefaultBrowserAppModelID failed.");
+ activateMgr->Release();
+ return false;
+ }
+ Log(L"App model id='%s'", appModelID);
+
+ // Hand off focus rights to the out-of-process activation server. Without
+ // this the metro interface won't launch.
+ hr = CoAllowSetForegroundWindow(activateMgr, NULL);
+ if (FAILED(hr)) {
+ Fail(L"CoAllowSetForegroundWindow result %X", hr);
+ activateMgr->Release();
+ return false;
+ }
+
+ Log(L"Harness process id: %d", GetCurrentProcessId());
+
+ // Because we can't pass command line args, we store params in a
+ // tests.ini file in dist/bin which the browser picks up on launch.
+ char path[MAX_PATH];
+ if (!GetModuleFileNameA(NULL, path, MAX_PATH)) {
+ Fail(L"GetModuleFileNameA errorno=%d", GetLastError());
+ activateMgr->Release();
+ return false;
+ }
+ char* slash = strrchr(path, '\\');
+ if (!slash)
+ return false;
+ *slash = '\0'; // no trailing slash
+ CStringA testFilePath = path;
+ testFilePath += "\\tests.ini";
+
+ HANDLE hTestFile = CreateFileA(testFilePath, GENERIC_WRITE,
+ 0, NULL, CREATE_ALWAYS,
+ FILE_ATTRIBUTE_NORMAL,
+ NULL);
+ if (hTestFile == INVALID_HANDLE_VALUE) {
+ Fail(L"CreateFileA errorno=%d", GetLastError());
+ activateMgr->Release();
+ return false;
+ }
+
+ CStringA asciiParams = sAppParams;
+ if (!WriteFile(hTestFile, asciiParams, asciiParams.GetLength(), NULL, 0)) {
+ CloseHandle(hTestFile);
+ Fail(L"WriteFile errorno=%d", GetLastError());
+ activateMgr->Release();
+ return false;
+ }
+ FlushFileBuffers(hTestFile);
+ CloseHandle(hTestFile);
+
+ // Launch firefox
+ hr = activateMgr->ActivateApplication(appModelID, L"", AO_NOERRORUI, &processID);
+ if (FAILED(hr)) {
+ Fail(L"ActivateApplication result %X", hr);
+ activateMgr->Release();
+ return false;
+ }
+
+ Log(L"Activation succeeded. processid=%d", processID);
+
+ HANDLE child = OpenProcess(SYNCHRONIZE, FALSE, processID);
+ if (!child) {
+ Fail(L"Couldn't find child process. (%d)", GetLastError());
+ activateMgr->Release();
+ return false;
+ }
+
+ Log(L"Waiting on child process...");
+
+ MSG msg;
+ DWORD waitResult = WAIT_TIMEOUT;
+ while ((waitResult = WaitForSingleObject(child, 10)) != WAIT_OBJECT_0) {
+ if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
+ TranslateMessage(&msg);
+ DispatchMessage(&msg);
+ }
+ }
+
+ Log(L"Exiting.");
+ activateMgr->Release();
+ DeleteFileA(testFilePath);
+ return true;
+}
+
+int wmain(int argc, WCHAR* argv[])
+{
+ CoInitialize(NULL);
+
+ int idx;
+ for (idx = 1; idx < argc; idx++) {
+ sAppParams.Append(argv[idx]);
+ sAppParams.Append(L" ");
+ }
+ sAppParams.Trim();
+ Log(L"args: '%s'", sAppParams);
+ Launch();
+
+ CoUninitialize();
+ return 0;
+}
diff --git a/browser/metro/shell/tileresources/Resources.resw b/browser/metro/shell/tileresources/Resources.resw
new file mode 100644
index 000000000000..175b01970edb
--- /dev/null
+++ b/browser/metro/shell/tileresources/Resources.resw
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Custom String Value
+
+
diff --git a/browser/metro/shell/tileresources/layout.resfiles b/browser/metro/shell/tileresources/layout.resfiles
new file mode 100644
index 000000000000..7dc302b5cf3e
--- /dev/null
+++ b/browser/metro/shell/tileresources/layout.resfiles
@@ -0,0 +1,47 @@
+images\PinnedSiteLogo.png
+images\PinnedSiteLogo.scale-100.png
+images\PinnedSiteLogo.scale-140.png
+images\PinnedSiteLogo.scale-180.png
+images\PinnedSiteLogo.scale-80.png
+images\PinnedSiteSmallLogo.png
+images\PinnedSiteSmallLogo.scale-100.png
+images\PinnedSiteSmallLogo.scale-140.png
+images\PinnedSiteSmallLogo.scale-180.png
+images\PinnedSiteSmallLogo.scale-80.png
+images\smallLogo.contrast-black_scale-100.png
+images\smallLogo.contrast-black_scale-140.png
+images\smallLogo.contrast-black_scale-180.png
+images\smallLogo.contrast-black_scale-80.png
+images\smallLogo.contrast-white_scale-100.png
+images\smallLogo.contrast-white_scale-140.png
+images\smallLogo.contrast-white_scale-180.png
+images\smallLogo.contrast-white_scale-80.png
+images\smallLogo.png
+images\smallLogo.scale-100.png
+images\smallLogo.scale-140.png
+images\smallLogo.scale-180.png
+images\smallLogo.scale-80.png
+images\splashscreen.contrast-black_scale-100.png
+images\splashscreen.contrast-black_scale-140.png
+images\splashscreen.contrast-black_scale-180.png
+images\splashscreen.contrast-white_scale-100.png
+images\splashscreen.contrast-white_scale-140.png
+images\splashscreen.contrast-white_scale-180.png
+images\splashscreen.png
+images\splashscreen.scale-100.png
+images\splashscreen.scale-140.png
+images\splashscreen.scale-180.png
+images\tileLogo.contrast-black_scale-100.png
+images\tileLogo.contrast-black_scale-140.png
+images\tileLogo.contrast-black_scale-180.png
+images\tileLogo.contrast-black_scale-80.png
+images\tileLogo.contrast-white_scale-100.png
+images\tileLogo.contrast-white_scale-140.png
+images\tileLogo.contrast-white_scale-180.png
+images\tileLogo.contrast-white_scale-80.png
+images\tileLogo.png
+images\tileLogo.scale-100.png
+images\tileLogo.scale-140.png
+images\images\tileLogo.scale-180.png
+images\tileLogo.scale-80.png
+xaml\MainPage.xaml
diff --git a/browser/metro/shell/tileresources/resources.resfiles b/browser/metro/shell/tileresources/resources.resfiles
new file mode 100644
index 000000000000..e5da0c4fb339
--- /dev/null
+++ b/browser/metro/shell/tileresources/resources.resfiles
@@ -0,0 +1 @@
+Resources.resw
diff --git a/browser/metro/theme/Makefile.in b/browser/metro/theme/Makefile.in
new file mode 100644
index 000000000000..1b4da71aeab5
--- /dev/null
+++ b/browser/metro/theme/Makefile.in
@@ -0,0 +1,12 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DEPTH = ../../..
+topsrcdir = @top_srcdir@
+srcdir = @srcdir@
+VPATH = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+include $(topsrcdir)/config/rules.mk
diff --git a/browser/metro/theme/about.css b/browser/metro/theme/about.css
new file mode 100644
index 000000000000..f3daef912624
--- /dev/null
+++ b/browser/metro/theme/about.css
@@ -0,0 +1,50 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+html {
+ background: #f0f0f0;
+ padding: 0 1em;
+ font-family: "Nokia Sans", Tahoma, sans-serif !important;
+ font-size: 100% !important;
+}
+
+body {
+ color: black;
+ position: relative;
+ min-width: 330px;
+ max-width: 50em;
+ margin: 1em auto;
+ border: 1px solid gray;
+ border-radius: 10px;
+ padding: 3em;
+ -moz-padding-start: 30px;
+ background: white;
+}
+
+.aboutPageWideContainer {
+ max-width: 80%;
+}
+
+#aboutLogoContainer {
+ border: 1px solid lightgray;
+ width: 300px;
+ margin-bottom: 2em;
+}
+
+img {
+ border: 0;
+}
+
+#version {
+ font-weight: bold;
+ color: #909090;
+ margin: -24px 0 9px 17px;
+}
+
+ul {
+ margin: 0;
+ -moz-margin-start: 1.5em;
+ padding: 0;
+ list-style: square;
+}
diff --git a/browser/metro/theme/aboutPage.css b/browser/metro/theme/aboutPage.css
new file mode 100644
index 000000000000..71bb7405b1b1
--- /dev/null
+++ b/browser/metro/theme/aboutPage.css
@@ -0,0 +1,72 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#aboutLogoContainer {
+ width: 300px;
+}
+
+#version {
+ font-weight: bold;
+ color: #909090;
+ margin: -24px 20px 0 118px;
+}
+
+#update {
+ float: right;
+ padding: 8px;
+ margin-top: -32px;
+}
+
+body[dir="rtl"] #update {
+ float: left;
+}
+
+#update-message-checking,
+#update-message-none,
+#update-message-found {
+ display: none;
+ float: right;
+ padding: 8px;
+ margin-top: -32px;
+}
+
+body[dir="rtl"] #update-message-checking,
+body[dir="rtl"] #update-message-none,
+body[dir="rtl"] #update-message-found {
+ float: left;
+}
+
+#aboutLinks {
+ background-color: white;
+ padding: 5px;
+ border: 2px solid #e6e5e3;
+ font-size: 24px;
+}
+
+#aboutLinks > li {
+ clear: both;
+ border-bottom: 2px solid #e6e5e3;
+ list-style: none;
+ -moz-padding-end: 16px;
+}
+
+#aboutLinks > li:last-child {
+ border-bottom: 0;
+}
+
+#aboutLinks > li > a {
+ padding: 16px;
+ display: block;
+ color: #3a3834;
+ background: url("images/arrowright-16.png") right center no-repeat;
+}
+
+body[dir="rtl"] #aboutLinks > li > a {
+ background: url("images/arrowleft-16.png") left center no-repeat;
+}
+
+#aboutDetails {
+ margin-top: 15px;
+ font-size: 18px;
+}
diff --git a/browser/metro/theme/browser.css b/browser/metro/theme/browser.css
new file mode 100644
index 000000000000..8a9b068de92b
--- /dev/null
+++ b/browser/metro/theme/browser.css
@@ -0,0 +1,1131 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+%filter substitution
+%include defines.inc
+
+%define forward_transition_length 150ms
+%define forward_width 51px
+%define back_width 62px
+%define clipped_url_back_width 71px
+
+/* Sliding Toolbar/Tab Tray ------------------------------------------------- */
+
+#tray {
+ transition: transform 0.2s ease-out;
+ transform: translateY(-@tray_slide_height@);
+ width: 100%;
+}
+
+#progress-control {
+ display: block;
+ height: @progress_height@;
+ max-height: @progress_height@;
+ margin-bottom: -@progress_height@;
+ opacity: 1;
+ background: linear-gradient(to right, @progress_start_color@, @progress_end_color@);
+ transition-property: width;
+ transition-duration: .3s;
+ -moz-user-focus: ignore;
+}
+
+#progress-control:-moz-dir(rtl) {
+ background: linear-gradient(to left, @progress_start_color@, @progress_end_color@);
+}
+
+#progress-control[fade] {
+ opacity: 0;
+ transition-property: width, opacity;
+ transition-duration: .3s, .5s;
+ transition-timing-function: ease-in, ease-in;
+}
+
+/* in non-tabsonly mode the navigation bar and tab tray float over content. In
+ tabsonly mode they are always visible and offset content. */
+#tray:not([tabsonly=true]) {
+ position: fixed;
+}
+
+#tray[visible][expanded] {
+ transform: none;
+}
+
+#tray[startpage],
+#tray[visible]:not([expanded]) {
+ transform: translateY(-@tabs_height@);
+}
+
+/* Tabs --------------------------------------------------------------------- */
+
+#tabs-container {
+ background: @panel_dark_color@ @panel_dark_background@;
+ padding: 0;
+ -moz-padding-start: @metro_spacing_xnormal@;
+ width: 100%;
+}
+
+#tabs {
+ -moz-padding-start: @metro_spacing_large@;
+}
+
+#tabs .tabs-list {
+ display: block;
+ -moz-user-focus: ignore;
+ padding: 0;
+ background-color: transparent;
+ margin: 0;
+ overflow: auto;
+}
+
+#tabs > .tabs-scrollbox > .arrowscrollbox-scrollbox {
+ overflow: hidden;
+}
+
+#tabs[input="imprecise"] > .tabs-scrollbox > .scrollbutton-up,
+#tabs[input="imprecise"] > .tabs-scrollbox > .scrollbutton-down {
+ visibility: collapse !important;
+}
+#tabs > .tabs-scrollbox > .scrollbutton-up {
+ list-style-image: url("images/tab-arrows.png") !important;
+ -moz-image-region: rect(15px 58px 63px 14px) !important;
+}
+#tabs > .tabs-scrollbox > .scrollbutton-up:hover {
+ -moz-image-region: rect(14px 102px 62px 58px) !important;
+}
+#tabs > .tabs-scrollbox > .scrollbutton-up:active {
+ -moz-image-region: rect(14px 152px 62px 108px) !important;
+}
+#tabs > .tabs-scrollbox > .scrollbutton-up[disabled="true"] {
+ -moz-image-region: rect(15px 196px 63px 152px) !important;
+}
+
+#tabs > .tabs-scrollbox > .scrollbutton-down {
+ list-style-image: url("images/tab-arrows.png") !important;
+ -moz-image-region: rect(73px 58px 121px 14px) !important;
+}
+#tabs > .tabs-scrollbox > .scrollbutton-down:hover {
+ -moz-image-region: rect(72px 102px 120px 58px) !important;
+}
+#tabs > .tabs-scrollbox > .scrollbutton-down:active {
+ -moz-image-region: rect(72px 152px 120px 108px) !important;
+}
+#tabs > .tabs-scrollbox > .scrollbutton-down[disabled="true"] {
+ -moz-image-region: rect(73px 196px 121px 152px) !important;
+}
+
+
+@-moz-keyframes open-documenttab {
+ 0% {
+ opacity: 0;
+ transform: scale(0, 0);
+ }
+
+ 100% {
+ opacity: 1;
+ transform: scale(1, 1);
+ }
+}
+
+#tray:not([tabsonly=true]) documenttab > .documenttab-container {
+ animation: open-documenttab;
+ animation-duration: 0.4s;
+ animation-timing-function: ease-out;
+}
+
+#tray:not([tabsonly=true]) .documenttab-favicon {
+ visibility: collapse;
+}
+
+.documenttab-thumbnail {
+ margin: @metro_spacing_normal@ @metro_spacing_snormal@;
+ background: white none center top no-repeat;
+ background-size: cover;
+ width: @thumbnail_width@;
+ height: @thumbnail_height@;
+}
+
+.documenttab-title {
+ margin: @metro_spacing_normal@ @metro_spacing_snormal@;
+ margin-top: 0;
+ font-size: @metro_font_normal@;
+ width: @thumbnail_width@;
+ padding: 4px @metro_spacing_snormal@ 8px;
+
+ background: #000;
+ opacity: 0.95;
+ color: #fff;
+ box-shadow: 0 0 @metro_spacing_snormal@ rgba(0, 0, 0, 0.25);
+}
+
+.documenttab-crop {
+ background: transparent url("chrome://browser/skin/images/tab-crop.png") 50% 50% no-repeat;
+}
+
+.documenttab-selection {
+ background: transparent -moz-image-rect(url("chrome://browser/skin/images/tab-overlay.png"), 0%, 100%, 50%, 0%) 50% 50% no-repeat;
+}
+
+documenttab[selected=true] .documenttab-selection {
+ background: transparent -moz-image-rect(url("chrome://browser/skin/images/tab-overlay.png"), 50%, 100%, 100%, 0%) 50% 50% no-repeat;
+}
+
+.documenttab-close {
+ background: none !important;
+ padding: @metro_spacing_small@ !important;
+ margin-top: @metro_spacing_snormal@;
+ -moz-margin-end: @metro_spacing_xsmall@;
+ border-color: transparent !important;
+ list-style-image: url("chrome://browser/skin/images/closetab-default.png");
+}
+
+#tray[tabsonly=true] {
+ transform: none !important;
+}
+
+#tray[tabsonly=true] #tabs {
+ -moz-padding-start: @metro_spacing_small@;
+}
+
+#tray[tabsonly=true] #tabs-controls {
+ -moz-box-align: center;
+ -moz-box-orient: horizontal;
+ -moz-box-pack: end;
+ margin: 0;
+}
+
+#tray[tabsonly=true] #tabs-controls toolbarbutton {
+ margin-top: 0;
+ margin-bottom: 0;
+}
+
+#tray[tabsonly=true] documenttab {
+ height: @toolbar_height@;
+ margin: 0 -@tab_compression@;
+}
+
+#tray[tabsonly=true] documenttab:first-child {
+ -moz-margin-start: 0;
+}
+
+#tray[tabsonly=true] documenttab:last-child {
+ -moz-margin-end: 0;
+}
+
+#tray[tabsonly=true] .documenttab-thumbnail,
+#tray[tabsonly=true] .documenttab-selection,
+#tray[tabsonly=true] .documenttab-crop {
+ visibility: collapse;
+}
+
+#tray[tabsonly=true] .documenttab-container {
+ display: -moz-box;
+ -moz-box-orient: horizontal;
+ -moz-box-align: center;
+ padding: 0 @tab_spacing@;
+}
+
+#tray[tabsonly=true] .documenttab-favicon {
+ -moz-margin-start: @metro_spacing_normal@;
+ -moz-margin-end: @metro_spacing_snormal@;
+}
+
+#tray[tabsonly=true] .documenttab-title {
+ padding: 0;
+ margin: 0;
+ height: auto;
+ background: 0 none;
+ opacity: 1;
+ box-shadow: none;
+ width: @tab_inner_width@;
+}
+
+#tray[tabsonly=true] .documenttab-close {
+ list-style-image: url("chrome://browser/skin/images/closetab-tab.png");
+ position: relative;
+ padding: 0 !important;
+ z-index: 1;
+}
+
+#tray[tabsonly=true] documenttab[selected=true] {
+ background-color: @panel_light_color@;
+ background-image: url("chrome://browser/skin/images/tab-selection-left.png"),
+ url("chrome://browser/skin/images/tab-selection-right.png"),
+ @panel_light_background@;
+ background-position: left top, right top, center center;
+ background-repeat: no-repeat, no-repeat, repeat;
+}
+
+#tray[tabsonly=true] documenttab[selected=true] .documenttab-close {
+ list-style-image: url("chrome://browser/skin/images/closetab-tabselected.png");
+}
+
+#page,
+.selection-overlay {
+ -moz-stack-sizing: ignore;
+}
+
+.selection-overlay:-moz-focusring {
+ outline: 0 !important;
+}
+
+.selection-overlay-hidden {
+ display: none;
+}
+
+#tray[tabsonly=true] documenttab[selected=true] .documenttab-title {
+ color: #000;
+}
+
+#tabs-controls {
+ margin-top: @metro_spacing_small@;
+ -moz-box-align: start;
+ -moz-box-orient: vertical;
+ padding: 0 @metro_spacing_small@;
+}
+
+#tabs-controls toolbarbutton {
+ margin: @toolbar_vertical_spacing@ @toolbar_horizontal_spacing@;
+}
+
+#newtab-button {
+ list-style-image: url("images/newtab-default.png");
+}
+
+/* Toolbar ------------------------------------------------------------------ */
+
+#toolbar-container {
+ background: @panel_dark_color@ @panel_dark_background@;
+ border-bottom: @border_width_small@ solid @border_color@;
+ -moz-padding-end: @padding_large@;
+ width: 100%;
+}
+
+#toolbar-container[filtering],
+#toolbar-container[startpage] {
+ border-bottom-width: 0;
+}
+
+#toolbar {
+ -moz-appearance: none;
+ -moz-box-align: center;
+ padding: 0;
+ -moz-padding-end: @metro_spacing_xxxlarge@;
+ -moz-padding-start: @metro_spacing_xxnormal@;
+ border: none;
+ border-top: @metro_border_thin@ solid #293642;
+ background-color: @panel_light_color@;
+ background-image: url("chrome://browser/skin/images/tab-selection-right.png"),
+ linear-gradient(rgba(255, 255, 255, 0.75), rgba(255, 255, 255, 0.5)),
+ @panel_light_background@;
+ background-repeat: no-repeat, repeat-x;
+ background-position: right bottom;
+ min-height: @toolbar_height@;
+}
+
+#toolbar[dir=ltr] {
+ background-position: left bottom;
+}
+
+#toolbar toolbarbutton {
+ margin: 0 @toolbar_horizontal_spacing@;
+}
+
+/* Unified back-forward buttons */
+/* TODO: Pull code from mainline firefox to support RTL. */
+#unified-back-forward-button {
+ -moz-box-align: center;
+ position: relative;
+ z-index: 1;
+}
+
+#back-button {
+ -moz-appearance: none;
+ margin: 0 !important;
+ margin-right: -@metro_spacing_normal@ !important;
+ list-style-image: url(chrome://browser/skin/images/back.png);
+ -moz-image-region: rect(0 48px 48px 0);
+ position: relative;
+ z-index: 1;
+ padding: 0 !important;
+ min-height: 48px !important;
+ max-height: 48px !important;
+}
+
+#back-button[disabled="true"] {
+ -moz-image-region: rect(0 96px 48px 48px);
+}
+
+#forward-button {
+ background: linear-gradient(to bottom, rgba(255, 255, 255, 0.7), rgba(255, 255, 255, 0.5)), @panel_light_background@;
+ border: @metro_border_thick@ solid rgb(192, 198, 204);
+ margin: 0 !important;
+ padding: 0 !important;
+ -moz-padding-start: 17px !important;
+ -moz-padding-end: 7px !important;
+ transition: opacity @forward_transition_length@ ease-out;
+ list-style-image: url(chrome://browser/skin/images/forward.png);
+}
+
+/* XXX: Hack to move the image up one pixel because
+ it's not vertically centered for some reason. */
+#forward-button image {
+ margin: -1px 0 1px 0 !important;
+}
+
+#unified-back-forward-button > #forward-button[disabled="true"] {
+ opacity: 0;
+}
+
+/* URL bar */
+#unified-back-forward-button + #urlbar-container {
+ margin: 0;
+ padding: 0;
+ padding-left: @back_width@;
+ -moz-margin-start: -@forward_width@;
+ -moz-margin-end: @metro_spacing_normal@;
+ position: relative;
+ pointer-events: none;
+
+ border: @metro_border_thick@ solid @urlbar_border_color@;
+ -moz-border-start: 0 none;
+ background: @field_background_color@;
+}
+
+#unified-back-forward-button + #urlbar-container > #urlbar {
+ -moz-border-start: none;
+ pointer-events: all;
+ transition: margin-left @forward_transition_length@ ease-out;
+}
+
+#unified-back-forward-button[forwarddisabled="true"] + #urlbar-container {
+ clip-path: url("chrome://browser/content/browser.xul#back-button-clip-path");
+ padding-left: @clipped_url_back_width@;
+}
+
+#unified-back-forward-button[forwarddisabled="true"] + #urlbar-container > #urlbar {
+ margin-left: -@forward_width@;
+}
+
+/* Identity widget */
+#identity-icon {
+ width: @metro_spacing_xxnormal@;
+ height: @metro_spacing_xxnormal@;
+ margin: 0;
+ -moz-margin-end: @metro_spacing_small@;
+ padding: 0;
+ list-style-image: url(chrome://browser/skin/images/identity-icons-generic.png);
+}
+
+#identity-box[mode="verifiedDomain"] > #identity-icon[pageproxystate="valid"] {
+ list-style-image: url(chrome://browser/skin/images/identity-icons-https.png);
+}
+
+#identity-box[mode="verifiedIdentity"] > #identity-icon[pageproxystate="valid"] {
+ list-style-image: url(chrome://browser/skin/identity-icons-https-ev.png);
+}
+
+/* Main URL textbox */
+#urlbar-edit {
+ margin: 0 !important;
+ min-height: @urlbar_edit_height@;
+ -moz-appearance: none !important;
+ border-radius: 0;
+ border: 0 none !important;
+ padding: 0 !important;
+}
+
+#urlbar-edit :invalid {
+ box-shadow: none;
+}
+
+/* Combined stop-reload button */
+#tool-reload {
+ list-style-image: url("chrome://browser/skin/images/reload.png");
+}
+
+#tool-stop {
+ list-style-image: url("chrome://browser/skin/images/stop-hdpi.png");
+}
+
+#urlbar-icons[mode="loading"] > #tool-reload {
+ visibility: collapse;
+}
+
+#urlbar-icons[mode="edit"] > #tool-stop,
+#urlbar-icons[mode="view"] > #tool-stop {
+ visibility: collapse;
+}
+
+/* Toggle that displays the tab bar */
+#toolbar-transition {
+ -moz-padding-end: @metro_spacing_snormal@;
+ background: @panel_dark_color@ @panel_dark_background@;
+}
+
+#tool-new-tab {
+ margin: 0;
+ -moz-margin-start: -@metro_spacing_normal@;
+ list-style-image: url("images/newtab-default.png");
+ transition: opacity ease-out 0.2s;
+}
+
+/* Hide the tab toggle if the tabs are visible */
+#tray[visible][expanded] #tool-new-tab {
+ opacity: 0;
+}
+
+/* Hide the tab toggle if we're showing classic tabs or we're snap-viewed. */
+#toolbar[viewstate="snapped"],
+#tray[tabsonly=true] #toolbar {
+ background: @panel_light_color@ @panel_light_background@;
+ -moz-padding-end: 0;
+}
+
+#toolbar-container[viewstate="snapped"],
+#tray[tabsonly=true] #toolbar-container {
+ -moz-padding-end: 0;
+}
+
+#toolbar-transition[viewstate="snapped"],
+#tray[tabsonly=true] #toolbar-transition {
+ visibility: collapse;
+}
+
+/* If we're in the small snap view, compress and simplify the UI. */
+#tray[visible][expanded][viewstate="snapped"] {
+ margin-top: -@tabs_height@ !important;
+}
+
+#toolbar[viewstate="snapped"] {
+ -moz-padding-end: 0;
+}
+
+#unified-back-forward-button[viewstate="snapped"] + #urlbar-container {
+ -moz-margin-end: 0;
+}
+
+/* App Bar ----------------------------------------------------------------- */
+
+appbar {
+ display: block;
+ position: fixed;
+ height: @toolbar_height@;
+ bottom: 0;
+ transform: translateY(@toolbar_height@);
+ transition: transform 0.2s ease-out;
+ width: 100%;
+}
+
+appbar toolbar {
+ border-top: 1px solid @appbar_top_border@;
+ border-bottom: 0px;
+ height: @toolbar_height@;
+ -moz-appearance: none;
+ background-color: @appbar_color@;
+ -moz-box-align: center;
+ width: 100%;
+}
+
+appbar toolbarbutton {
+ float: left;
+ border-width: 0px;
+ margin: 0 @toolbar_horizontal_spacing@;
+ padding: 0;
+ /* Don't inherit background-color from toolbarbutton[checked="true"] */
+ background-color: transparent;
+}
+
+appbar toolbarbutton[disabled="true"] {
+ visibility: collapse;
+}
+
+#appbar:not([viewstate="snapped"])[visible] {
+ transform: none;
+}
+
+#appbar toolbarbutton {
+ list-style-image: url(chrome://browser/skin/images/appbar-icons.png);
+ -moz-image-region: rect(0px, 200px, 40px, 160px); /* Gear icon is default. */
+}
+#appbar toolbarbutton:hover {
+ -moz-image-region: rect(40px, 200px, 80px, 160px);
+}
+#appbar toolbarbutton:active {
+ -moz-image-region: rect(80px, 200px, 120px, 160px);
+}
+
+/* About flyout pane */
+
+#about-flyoutpanel {
+ width: 350px;
+ background-image:url('chrome://browser/skin/images/about-footer.png');
+ background-repeat: no-repeat;
+ background-attachment: fixed;
+ background-position: right bottom;
+}
+
+#about-flyoutpanel label.text-link {
+ text-decoration: none;
+ color: #1167bd;
+}
+
+#about-product-label {
+ font-weight: bold;
+}
+
+#about-version-label {
+ margin-top: 11pt;
+}
+
+#about-policy-label {
+ margin-top: 24pt;
+}
+
+/* Application-Specific */
+#download-button {
+ -moz-image-region: rect(0px, 40px, 40px, 0px) !important;
+}
+#download-button:hover {
+ -moz-image-region: rect(40px, 40px, 80px, 0px) !important;
+}
+#download-button:active {
+ -moz-image-region: rect(80px, 40px, 120px, 0px) !important;
+}
+
+#plugin-button {
+ -moz-image-region: rect(0px, 80px, 40px, 40px) !important;
+}
+#plugin-button:hover {
+ -moz-image-region: rect(40px, 80px, 80px, 40px) !important;
+}
+#plugin-button:active {
+ -moz-image-region: rect(80px, 80px, 120px, 40px) !important;
+}
+
+/* Page-Specific */
+#zoomout-button {
+ -moz-image-region: rect(0px, 120px, 40px, 80px) !important;
+}
+#zoomout-button:hover {
+ -moz-image-region: rect(40px, 120px, 80px, 80px) !important;
+}
+#zoomout-button:active {
+ -moz-image-region: rect(80px, 120px, 120px, 80px) !important;
+}
+
+#zoomin-button {
+ -moz-image-region: rect(0px, 160px, 40px, 120px) !important;
+}
+#zoomin-button:hover {
+ -moz-image-region: rect(40px, 160px, 80px, 120px) !important;
+}
+#zoomin-button:active {
+ -moz-image-region: rect(80px, 160px, 120px, 120px) !important;
+}
+
+#pin-button {
+ -moz-image-region: rect(0px, 240px, 40px, 200px) !important;
+}
+#pin-button:hover {
+ -moz-image-region: rect(40px, 240px, 80px, 200px) !important;
+}
+#pin-button:active {
+ -moz-image-region: rect(80px, 240px, 120px, 200px) !important;
+}
+#pin-button[checked="true"] {
+ -moz-image-region: rect(0px, 280px, 40px, 240px) !important;
+}
+
+#star-button {
+ -moz-image-region: rect(0px, 360px, 40px, 320px) !important;
+}
+#star-button:hover {
+ -moz-image-region: rect(40px, 360px, 80px, 320px) !important;
+}
+#star-button:active,
+#star-button[checked="true"] {
+ -moz-image-region: rect(80px, 360px, 120px, 320px) !important;
+}
+
+/* Start UI (Autocomplete + New Tab Page) ----------------------------------- */
+
+#start-container {
+ display: none;
+}
+
+#start-container[startpage],
+#start-container[filtering] {
+ display: -moz-box;
+}
+
+#start-scrollbox {
+ overflow: hidden;
+}
+
+/* if autocomplete is set, hide both start pages,
+ * else hide the autocomplete screen */
+#start-container[filtering] > .start-page,
+#start-container:not([filtering]) > #start-autocomplete {
+ visibility: collapse;
+}
+
+/* if snapped, hide the fullscreen awesome screen, if viewstate is anything
+ * other than snapped, hide the snapped awesome screen */
+#start[viewstate="snapped"],
+#snapped-start:not([viewstate="snapped"]) {
+ visibility: collapse;
+}
+
+/* Browser Content Ares ----------------------------------------------------- */
+
+/* Hide the browser while the start UI is visible */
+#content-viewport[startpage],
+#content-viewport[filtering] {
+ visibility: collapse;
+}
+
+#browsers {
+ background: white;
+}
+
+/* Panel UI ---------------------------------------------------------------- */
+
+#panel-container {
+ padding: 60px 40px;
+}
+
+#panel-close-button {
+ background: transparent;
+ border: 0 none;
+ -moz-appearance: none;
+ margin: 0;
+ -moz-margin-end: 40px;
+ list-style-image: url(chrome://browser/skin/images/back.png);
+ -moz-image-region: rect(0 48px 48px 0);
+ padding: 0;
+ min-height: 48px;
+ max-height: 48px;
+ -moz-box-pack: center;
+}
+
+#panel-close-button[disabled="true"] {
+ -moz-image-region: rect(0 96px 48px 48px);
+}
+
+#panel-view-switcher {
+ border: 0 none !important;
+ color: #000 !important;
+ background: transparent;
+ padding: 0;
+ font-size: @metro_font_xlarge@;
+ font-weight: 100;
+ margin: 0;
+}
+
+#panel-items {
+ padding-top: 20px;
+ -moz-padding-start: 88px;
+}
+
+/* Preferences Section - Panel UI ------------------------------------------ */
+
+#prefs-flyoutpanel {
+ width: 400px;
+}
+
+/* Lay out each in a single row */
+setting {
+ min-height: @touch_row@; /* row size */
+ -moz-box-align: center;
+ -moz-box-orient: horizontal;
+}
+
+/* ...except for some exceptions */
+.setting-expanded {
+ -moz-box-align: start;
+ -moz-box-orient: vertical;
+}
+
+setting > vbox {
+ -moz-box-flex: 1;
+}
+
+settings {
+ margin-top: 32px;
+}
+
+.settings-title {
+ font-weight: bold;
+}
+
+/* elements that are not in a group get special treatment */
+#prefs-flyoutpanel > setting {
+ margin-top: 16px;
+}
+#prefs-flyoutpanel > setting .preferences-title {
+ font-weight: bold
+}
+
+setting[type="integer"] > .preferences-alignment,
+setting[type="string"] > .preferences-alignment {
+ -moz-box-flex: 3;
+}
+
+setting[type="file"] > .preferences-alignment,
+setting[type="directory"] > .preferences-alignment {
+ -moz-box-align: center;
+}
+
+.options-box {
+ -moz-margin-start: 28px; /* sized based on the 32px addon image */
+}
+
+.options-box > setting:last-child {
+ border-bottom: 0;
+}
+
+.preferences-description {
+ font-size: @font_small@ !important;
+ color: grey;
+}
+
+.preferences-description:empty {
+ display: none;
+}
+
+/* Console Section - Panel UI ---------------------------------------------- */
+
+#console-filter-warnings,
+#console-filter-messages {
+ visibility: visible;
+}
+
+@media (max-width: 499px) {
+ #console-filter-warnings,
+ #console-filter-messages {
+ visibility: collapse;
+ }
+}
+
+.console-error-msg,
+.console-msg-text {
+ white-space: pre-wrap;
+}
+
+/* Identity popup -------------------------------------------------------- */
+
+#identity-popup-container {
+ padding: @padding_normal@; /* core spacing */
+ padding-bottom: @padding_xxxnormal@;
+}
+
+/* Popup Icons */
+#identity-popup-icon {
+ padding: 0;
+ list-style-image: url("chrome://browser/skin/images/identity-default-hdpi.png");
+}
+
+#identity-container[mode="verifiedIdentity"] > box > #identity-popup-icon {
+ list-style-image: url("chrome://browser/skin/images/identity-ev-hdpi.png");
+}
+
+#identity-container[mode="verifiedDomain"] > box > #identity-popup-icon {
+ list-style-image: url("chrome://browser/skin/images/identity-ssl-hdpi.png");
+}
+
+/* Popup Body Text */
+#identity-popup-content-box {
+ -moz-padding-start: @padding_normal@; /* core spacing */
+ font-size: @font_small@ !important;
+ white-space: pre-wrap;
+}
+
+/* let the text flow into a second row, if needed */
+#identity-popup-runBy-box {
+ display: block;
+}
+
+#identity-popup-content-box.unknownIdentity > box > #identity-popup-connectedToLabel,
+#identity-popup-content-box.unknownIdentity > #identity-popup-runBy-box,
+#identity-popup-content-box.unknownIdentity > box > #identity-popup-content-host,
+#identity-popup-content-box.verifiedIdentity > box > #identity-popup-connectedToLabel2,
+#identity-popup-content-box.verifiedDomain > box > #identity-popup-connectedToLabel2 {
+ display: none;
+}
+
+#identity-popup-encryption-label,
+#identity-popup-content-verifier {
+ font-size: @font_tiny@ !important;
+}
+
+#identity-popup-content-host,
+#identity-popup-content-owner {
+ font-weight: bold;
+}
+
+#identity-popup-encryption-icon {
+ list-style-image: url("chrome://browser/skin/images/unlocked-hdpi.png");
+}
+
+#identity-container[mode="verifiedIdentity"] > hbox > vbox > #identity-popup-encryption-icon ,
+#identity-container[mode="verifiedDomain"] > hbox > vbox > #identity-popup-encryption-icon {
+ list-style-image: url("chrome://browser/skin/images/locked-hdpi.png");
+}
+
+#identity-popup-encryption-box {
+ -moz-box-orient: vertical;
+ -moz-box-align: center;
+ -moz-box-pack: start;
+}
+
+#identity-popup-connected-box {
+ -moz-box-orient: horizontal;
+}
+
+#identity-popup-content-supplemental {
+ white-space: normal;
+}
+
+/* Context Menu ------------------------------------------------------------ */
+
+#context-commands richlistitem[disabled="true"] {
+ display: none;
+}
+
+/* Page Actions and Prompt ------------------------------------------------- */
+
+.action-buttons,
+#pageactions-container {
+ background: transparent;
+ border-top: @border_width_tiny@ solid rgb(205,205,205);
+ padding: 0;
+ -moz-user-focus: ignore;
+}
+
+.action-buttons,
+#pageactions-container {
+ display: inline-block;
+}
+
+.action-button,
+pageaction {
+ -moz-border-top-colors: white;
+ -moz-border-right-colors: rgb(175,175,175);
+ -moz-border-bottom-colors: rgb(175,175,175);
+ -moz-border-left-colors: white;
+ border-style: solid;
+ border-width: @border_width_tiny@ !important;
+ height: @touch_button_xlarge@;
+ min-height: @touch_button_xlarge@;
+ width: 100%;
+ min-width: @touch_action_minwidth@; /* keep the button from being too narrow */
+}
+
+.action-button {
+ -moz-box-align: center;
+}
+
+.action-button[disabled="true"] {
+ pointer-events: none;
+ color: #aaa !important;
+}
+
+.action-button[selected="true"] {
+ background: transparent;
+}
+
+/* Override button styles */
+.action-button {
+ margin: 0;
+ -moz-border-image: none !important;
+ border-radius: 0;
+ margin: 0;
+ background: transparent;
+}
+
+.action-button > .button-box {
+ padding: 0 @padding_small@ @padding_tiny@ @padding_xsmall@ !important;
+}
+
+.action-button > .button-box > .button-icon {
+ -moz-margin-end: @margin_normal@;
+}
+
+@media (min-width: 500px) {
+ .action-button,
+ pageaction {
+ width: 50%;
+ }
+
+ .action-button:last-child:nth-child(odd),
+ pageaction.odd.last-child {
+ width: 100%;
+ }
+}
+
+.action-button:not([disabled]):hover:active,
+pageaction:not([disabled]):hover:active {
+ background: url("chrome://browser/skin/images/popup-selected-item-hdpi.png") repeat-x !important;
+ background-origin: border-box !important;
+ background-clip: border-box !important;
+ -moz-border-top-colors: transparent;
+ -moz-border-left-colors: transparent;
+}
+
+pageaction > hbox > .pageaction-image {
+ width: 32px;
+ height: 32px;
+ -moz-margin-end: @margin_normal@;
+}
+
+pageaction:not([image]) > hbox >.pageaction-image {
+ width: 0;
+}
+
+.action-button,
+.pageaction-title {
+ font-size: @font_normal@ !important;
+ color: #414141 !important;
+}
+
+.pageaction-desc {
+ font-size: @font_tiny@ !important;
+ color: #414141;
+}
+
+.pageaction-desc[value=""] {
+ display: none;
+}
+
+#share-title {
+ font-size: @font_small@;
+ padding: @padding_small@;
+}
+
+/* Alert Popup -------------------------------------------------------------- */
+#alerts-container {
+ color: white;
+ background-color: #5e6166;
+ border: @border_width_small@ solid #767973;
+ border-radius: @border_radius_normal@;
+ box-shadow: black 0 @border_radius_tiny@ @border_radius_tiny@;
+ padding: @padding_normal@; /* core spacing on top/bottom */
+ margin-bottom: @margin_large@;
+ transition-property: opacity;
+ transition-duration: 0.5s;
+ opacity: 0;
+}
+
+#alerts-container.showing {
+ opacity: 1;
+}
+
+#alerts-title {
+ font-size: @font_small@ !important;
+}
+
+#alerts-text {
+ font-size: @font_xsmall@ !important;
+ white-space: pre;
+}
+
+#alerts-container {
+ -moz-margin-end: @margin_large@;
+}
+
+/* helperapp (save-as) popup ----------------------------------------------- */
+#helperapp-target {
+ font-size: @font_small@ !important;
+}
+
+/* Sync Setup ------------------------------------------------------------- */
+.syncsetup-code {
+ display: block !important;
+ margin: @metro_spacing_small@;
+ padding: @metro_spacing_xsmall@ @metro_spacing_snormal@;
+ background: @field_background_color@;
+ border: @metro_border_thick@ solid @field_foreground_color@ !important;
+ color: @field_foreground_color@;
+ font-size: @metro_font_large@ !important;
+ letter-spacing: 0.2em;
+ text-align: left;
+ width: 250px;
+ -moz-box-flex: 1;
+}
+
+.syncsetup-label {
+ color: #fff;
+}
+
+#syncsetup-customserver {
+ -moz-margin-start: @margin_xnormal@;
+}
+
+#syncsetup-waiting {
+ padding: 2em 0 0 0;
+}
+
+#syncsetup-waiting-top {
+ padding: 1em;
+}
+
+/* content scrollbars */
+.scroller {
+ opacity: 0;
+ background-color: rgba(0, 0, 0, 0.4) !important;
+ -moz-border-top-colors: none !important;
+ -moz-border-bottom-colors: none !important;
+ -moz-border-right-colors: none !important;
+ -moz-border-left-colors: none !important;
+ border-radius: @border_radius_tiny@;
+ border: @border_width_tiny@ solid rgba(255, 255, 255, 0.4) !important;
+}
+
+.scroller[panning="true"] {
+ opacity: 1;
+}
+
+.scroller[orient="vertical"] {
+ min-width: @scroller_thickness@;
+ width: @scroller_thickness@;
+ min-height: @scroller_minimum@;
+}
+
+.scroller[orient="horizontal"] {
+ min-height: @scroller_thickness@;
+ height: @scroller_thickness@;
+ min-width: @scroller_minimum@;
+}
+
+/* Text selection handles */
+
+#selectionhandle-start,
+#selectionhandle-end {
+ border: 0px solid gray;
+ padding: 0px;
+ margin-top: -30px;
+ margin-left: -18px;
+}
+
+#selectionhandle-start {
+ list-style-image: url("chrome://browser/skin/images/selection-monocle.png");
+}
+
+#selectionhandle-end {
+ list-style-image: url("chrome://browser/skin/images/selection-monocle.png");
+}
+
+/* Capture picker ------------------------------------------------------------- */
+
+#capturepicker-video {
+ border: @border_width_tiny@ solid white;
+}
+
+#capturepicker-container {
+ margin: @margin_normal@;
+}
+
+#capturepicker-container.vertical {
+ height: 330px;
+}
diff --git a/browser/metro/theme/config.css b/browser/metro/theme/config.css
new file mode 100644
index 000000000000..64ccf41bf379
--- /dev/null
+++ b/browser/metro/theme/config.css
@@ -0,0 +1,97 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@media (max-width: 499px) {
+ #editor-container > hbox {
+ -moz-box-orient: vertical;
+ }
+}
+
+richlistitem {
+ -moz-box-align: center;
+}
+
+richlistitem .preferences-title {
+ pointer-events: none;
+ min-width: 200px;
+ -moz-box-flex: 1;
+ margin-right: 8px;
+}
+
+/* XXX look + sync */
+richlistitem[default="false"] .preferences-title {
+ font-weight: bold;
+}
+
+richlistitem .preferences-value {
+ min-width: 200px;
+ pointer-events: none;
+ -moz-box-flex: 4;
+ text-align: end;
+ color: grey;
+}
+
+/* Editor */
+#editor-row {
+ padding: 0;
+ background: #E9E9E9;
+}
+
+#editor {
+ border-bottom: 1px solid rgb(207,207,207);
+}
+
+#editor > hbox > #editor-name,
+#editor > hbox > #editor-cancel,
+#editor > hbox > #editor-done {
+ display: none;
+}
+
+#editor-container > #editor > hbox > #editor-name,
+#editor-container > #editor > hbox > #editor-cancel,
+#editor-container > #editor > hbox > #editor-done {
+ display: -moz-box;
+}
+
+#editor-container > #editor > hbox > #editor-reset {
+ display: none;
+}
+
+#editor-container > hbox > label {
+ pointer-events: none;
+ color: black;
+}
+
+#editor + richlistitem {
+ display: none;
+}
+
+#editor[default="false"] .preferences-title {
+ font-weight: bold;
+}
+
+#editor-setting setting {
+ border-color: transparent !important;
+}
+
+#editor-setting[type="string"] .setting-input {
+ -moz-box-flex: 4;
+}
+
+#editor-setting[type="string"] .setting-input > textbox {
+ -moz-box-flex: 1;
+}
+
+/* bug 647650: keep 'text-align: right' here instead of using start/end since
+ * the field should looks like ltr as much as possible
+ */
+#editor-setting[type="string"] .setting-input > textbox:-moz-locale-dir(rtl) {
+ direction: ltr;
+ text-align: right;
+}
+
+#editor-buttons {
+ margin: 2px;
+}
+
diff --git a/browser/metro/theme/content.css b/browser/metro/theme/content.css
new file mode 100644
index 000000000000..3de8529a0f2f
--- /dev/null
+++ b/browser/metro/theme/content.css
@@ -0,0 +1,320 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+%filter substitution
+%include defines.inc
+
+@namespace url("http://www.w3.org/1999/xhtml");
+@namespace xul url("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul");
+
+/* make clicking on links stand out a bit (bug 532206) */
+* > *:not(embed):focus, * > *:focus > font {
+ outline: 1px solid #8db8d8 !important;
+}
+
+*:-moz-any-link:focus {
+ outline-offset: -2px;
+}
+
+/* Style the scrollbars */
+html xul|scrollbar {
+ display: none;
+}
+
+xul|window xul|scrollbar {
+ display: block;
+}
+
+xul|window xul|scrollbar[orient="vertical"] {
+ -moz-appearance: none !important;
+ opacity: 0;
+ position: relative;
+ margin-left: -8px;
+ min-width: 8px;
+ background-color: transparent !important;
+ background-image: none !important;
+ border: 0px solid transparent !important;
+}
+
+xul|window xul|scrollbar[orient="vertical"]:-moz-locale-dir(rtl) {
+ margin-left: 2px;
+ margin-right: -10px;
+}
+
+xul|window xul|scrollbar[orient="vertical"] xul|thumb {
+ max-width: 6px !important;
+ min-width: 6px !important;
+}
+
+xul|window xul|scrollbar[orient="horizontal"] {
+ -moz-appearance: none !important;
+ opacity: 0;
+ position: relative;
+ min-height: 8px;
+ margin-top: -8px;
+ background-color: transparent !important;
+ background-image: none !important;
+ border: 0px solid transparent !important;
+}
+
+xul|window xul|scrollbar[orient="horizontal"] xul|thumb {
+ max-height: 6px !important;
+ min-height: 6px !important;
+}
+
+xul|window xul|*[panning="true"] xul|scrollbar {
+ opacity: 1;
+}
+
+xul|window xul|scrollbox {
+ overflow-y: scroll;
+ overflow-x: scroll;
+}
+
+xul|window xul|scrollbarbutton {
+ min-height: 8px !important;
+ min-width: 8px !important;
+ -moz-appearance: none !important;
+ visibility: hidden;
+}
+
+xul|window xul|scrollbarbutton[sbattr="scrollbar-up-top"],
+xul|window xul|scrollbarbutton[sbattr="scrollbar-bottom-top"] {
+ display: none;
+}
+
+xul|window xul|scrollbar xul|thumb {
+ background-color: rgba(0, 0, 0, 0.4) !important;
+ -moz-border-top-colors: none !important;
+ -moz-border-bottom-colors: none !important;
+ -moz-border-right-colors: none !important;
+ -moz-border-left-colors: none !important;
+ border: 1px solid rgba(255, 255, 255, 0.4) !important;
+ border-radius: 3px;
+}
+
+select:not([size]):not([multiple]) > xul|scrollbar,
+select[size="1"] > xul|scrollbar,
+select:not([size]):not([multiple]) xul|scrollbarbutton,
+select[size="1"] xul|scrollbarbutton {
+ display: block;
+ margin-left: 0;
+ min-width: 16px;
+}
+
+/* Override inverse OS themes */
+select,
+button,
+xul|button,
+* > input:not([type="image"]) {
+ /* -moz-appearance: none !important; See bug 598421 for fixing the platform */
+ /*border-radius: 3px;*/
+}
+
+select[size],
+select[multiple],
+select[size][multiple],
+* > input:not([type="image"]) {
+
+ border-style: solid;
+ border-color: #7d7d7d;
+ color: #414141;
+ /*background: white -moz-linear-gradient(top, rgba(115,115,115,0.5) 0, rgba(215,215,215,0.5) 3px, rgba(255,255,255,0.2) 16px);*/
+}
+
+/* Selects are handled by the form helper, see bug 685197 */
+select option, select optgroup {
+ pointer-events: none;
+}
+
+input:-moz-placeholder {
+ color: GrayText;
+}
+
+select:not([size]):not([multiple]),
+select[size="0"],
+select[size="1"],
+* > input[type="button"],
+* > input[type="submit"],
+* > input[type="reset"],
+button {
+ border-style: solid;
+ border-color: #7d7d7d;
+ color: #414141;
+ /*background: white -moz-linear-gradient(top, rgba(255,255,255,0.2) 0, rgba(215,215,215,0.5) 18px, rgba(115,115,115,0.5) 100%);*/
+}
+
+input[type="checkbox"] {
+ background: white -moz-linear-gradient(top, rgba(115,115,115,0.5) 0, rgba(215,215,215,0.5) 2px, rgba(255,255,255,0.2) 6px);
+}
+
+input[type="radio"] {
+ background: -moz-radial-gradient(6px 6px, cover, rgba(255,255,255,0.2) 3px, rgba(195,195,195,0.5) 5px, rgba(115,115,115,0.5) 100%);
+}
+
+select {
+ border-width: 1px;
+ padding: 1px;
+}
+
+select:not([size]):not([multiple]),
+select[size="0"],
+select[size="1"] {
+ padding: 0 1px 0 1px;
+}
+
+* > input:not([type="image"]) {
+ border-width: 1px;
+ padding: 1px;
+}
+
+textarea {
+ /*
+ resize: none;
+ border-width: 1px;
+ padding: 2px 1px 2px 1px;*/
+}
+
+input[type="button"],
+input[type="submit"],
+input[type="reset"],
+button {
+ border-width: 1px;
+ padding: 0 7px 0 7px;
+}
+
+input[type="radio"],
+input[type="checkbox"] {
+ max-width: 14px;
+ max-height: 14px;
+ border: 1px solid #a7a7a7 !important;
+ padding: 2px 1px 2px 1px;
+}
+
+select > button {
+ border-width: 1px !important;
+ margin: 0px !important;
+ padding: 0px !important;
+ border-radius: 0;
+ color: #414141;
+ /*
+ background-size: 100% 90%;
+ background-color: transparent;
+ background-image: -moz-radial-gradient(bottom left, #bbbbbb 40%, #f5f5f5) !important;
+ background-position: -15px center !important;
+ background-repeat: no-repeat !important;
+ */
+ /* Use to position an svg arrow on