Bug 687729 - Move more code from common-ui.js out of the startup path [r=mfinkle]
authorMatt Brubeck <mbrubeck@mozilla.com>
Tue, 20 Sep 2011 13:25:28 -0700
changeset 77217 e0ea0a7ecbee994529bd5a4e4cd231791cb3fffe
parent 77216 355ffd653a9df1cb8ed3e78a09ebbe2e4834c199
child 77218 397abdbd54a81de1f22efa4094eb599772d13c99
push id3
push userfelipc@gmail.com
push dateFri, 30 Sep 2011 20:09:13 +0000
reviewersmfinkle
bugs687729
milestone9.0a1
Bug 687729 - Move more code from common-ui.js out of the startup path [r=mfinkle]
mobile/chrome/content/CharsetMenu.js
mobile/chrome/content/PageActions.js
mobile/chrome/content/SelectionHelper.js
mobile/chrome/content/WebappsUI.js
mobile/chrome/content/browser-scripts.js
mobile/chrome/content/browser-ui.js
mobile/chrome/content/common-ui.js
mobile/chrome/jar.mn
new file mode 100644
--- /dev/null
+++ b/mobile/chrome/content/CharsetMenu.js
@@ -0,0 +1,122 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Mobile Browser.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Wes Johnston <wjohnston@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+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().documentCharsetInfo.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().documentCharsetInfo.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;
+    MenuListHelperUI.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);
+  }
+};
new file mode 100644
--- /dev/null
+++ b/mobile/chrome/content/PageActions.js
@@ -0,0 +1,273 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Mobile Browser.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Matt Brubeck <mbrubeck@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+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);
+    this.register("pageaction-search", BrowserSearch.updatePageSearchEngines, BrowserSearch);
+    this.register("pageaction-webapps-install", WebappsUI.updateWebappsInstall, WebappsUI);
+
+    CharsetMenu.init();
+  },
+
+  handleEvent: function handleEvent(aEvent) {
+    switch (aEvent.type) {
+      case "click":
+        getIdentityHandler().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._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"],
+
+  _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.
+  },
+
+  savePageAsPDF: function saveAsPDF() {
+    let browser = Browser.selectedBrowser;
+    let fileName = ContentAreaUtils.getDefaultFileName(browser.contentTitle, browser.documentURI, null, null);
+    fileName = fileName.trim() + ".pdf";
+
+    let dm = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
+    let downloadsDir = dm.defaultDownloadsDirectory;
+
+#ifdef ANDROID
+    // Create the final destination file location
+    let file = downloadsDir.clone();
+    file.append(fileName);
+    file.createUnique(file.NORMAL_FILE_TYPE, 0666);
+#else
+    let picker = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+    picker.init(window, Strings.browser.GetStringFromName("pageactions.saveas.pdf"), Ci.nsIFilePicker.modeSave);
+    picker.appendFilter("PDF", "*.pdf");
+    picker.defaultExtension = "pdf";
+
+    picker.defaultString = fileName;
+
+    picker.displayDirectory = downloadsDir;
+    let rv = picker.show();
+    if (rv == Ci.nsIFilePicker.returnCancel)
+      return;
+
+    let file = picker.file;
+#endif
+    fileName = file.leafName;
+
+    // We must manually add this to the download system
+    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)"
+    );
+
+    let current = browser.currentURI.spec;
+    stmt.params.name = fileName;
+    stmt.params.source = current;
+    stmt.params.target = Services.io.newFileURI(file).spec;
+    stmt.params.startTime = Date.now() * 1000;
+    stmt.params.endTime = Date.now() * 1000;
+    stmt.params.state = Ci.nsIDownloadManager.DOWNLOAD_NOTSTARTED;
+    stmt.params.referrer = current;
+    stmt.execute();
+    stmt.finalize();
+
+    let newItemId = db.lastInsertRowID;
+    let download = dm.getDownload(newItemId);
+    try {
+      DownloadsView.downloadStarted(download);
+    }
+    catch(e) {}
+    Services.obs.notifyObservers(download, "dl-start", null);
+
+#ifdef ANDROID
+    let tmpDir = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties).get("TmpD", Ci.nsIFile);  
+    
+    file = tmpDir.clone();
+    file.append(fileName);
+
+#endif
+
+    let data = {
+      type: Ci.nsIPrintSettings.kOutputFormatPDF,
+      id: newItemId,
+      referrer: current,
+      filePath: file.path
+    };
+
+    Browser.selectedBrowser.messageManager.sendAsyncMessage("Browser:SaveAs", data);
+  },
+
+  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;
+
+    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");
+  }
+};
new file mode 100644
--- /dev/null
+++ b/mobile/chrome/content/SelectionHelper.js
@@ -0,0 +1,202 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Mobile Browser.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Mark Finkle <mfinkle@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+var SelectionHelper = {
+  enabled: true,
+  popupState: null,
+  target: null,
+  deltaX: -1,
+  deltaY: -1,
+
+  get _start() {
+    delete this._start;
+    return this._start = document.getElementById("selectionhandle-start");
+  },
+
+  get _end() {
+    delete this._end;
+    return this._end = document.getElementById("selectionhandle-end");
+  },
+
+  showPopup: function sh_showPopup(aMessage) {
+    if (!this.enabled || aMessage.json.types.indexOf("content-text") == -1)
+      return false;
+
+    this.popupState = aMessage.json;
+    this.popupState.target = aMessage.target;
+
+    this._start.customDragger = {
+      isDraggable: function isDraggable(target, content) { return { x: true, y: false }; },
+      dragStart: function dragStart(cx, cy, target, scroller) {},
+      dragStop: function dragStop(dx, dy, scroller) { return false; },
+      dragMove: function dragMove(dx, dy, scroller) { return false; }
+    };
+
+    this._end.customDragger = {
+      isDraggable: function isDraggable(target, content) { return { x: true, y: false }; },
+      dragStart: function dragStart(cx, cy, target, scroller) {},
+      dragStop: function dragStop(dx, dy, scroller) { return false; },
+      dragMove: function dragMove(dx, dy, scroller) { return false; }
+    };
+
+    this._start.addEventListener("TapUp", this, true);
+    this._end.addEventListener("TapUp", this, true);
+
+    messageManager.addMessageListener("Browser:SelectionRange", this);
+    messageManager.addMessageListener("Browser:SelectionCopied", this);
+
+    this.popupState.target.messageManager.sendAsyncMessage("Browser:SelectionStart", { x: this.popupState.x, y: this.popupState.y });
+
+    // Hide the selection handles
+    window.addEventListener("TapDown", this, true);
+    window.addEventListener("resize", this, true);
+    window.addEventListener("keypress", this, true);
+    Elements.browsers.addEventListener("URLChanged", this, true);
+    Elements.browsers.addEventListener("SizeChanged", this, true);
+    Elements.browsers.addEventListener("ZoomChanged", this, true);
+
+    let event = document.createEvent("Events");
+    event.initEvent("CancelTouchSequence", true, false);
+    this.popupState.target.dispatchEvent(event);
+
+    return true;
+  },
+
+  hide: function sh_hide(aEvent) {
+    if (this._start.hidden)
+      return;
+
+    let pos = this.popupState.target.transformClientToBrowser(aEvent.clientX || 0, aEvent.clientY || 0);
+    let json = {
+      x: pos.x,
+      y: pos.y
+    };
+
+    try {
+      this.popupState.target.messageManager.sendAsyncMessage("Browser:SelectionEnd", json);
+    } catch (e) {
+      Cu.reportError(e);
+    }
+
+    this.popupState = null;
+
+    this._start.hidden = true;
+    this._end.hidden = true;
+
+    this._start.removeEventListener("TapUp", this, true);
+    this._end.removeEventListener("TapUp", this, true);
+
+    messageManager.removeMessageListener("Browser:SelectionRange", this);
+
+    window.removeEventListener("TapDown", this, true);
+    window.removeEventListener("resize", 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);
+  },
+
+  handleEvent: function handleEvent(aEvent) {
+    switch (aEvent.type) {
+      case "TapDown":
+        if (aEvent.target == this._start || aEvent.target == this._end) {
+          this.target = aEvent.target;
+          this.deltaX = (aEvent.clientX - this.target.left);
+          this.deltaY = (aEvent.clientY - this.target.top);
+          window.addEventListener("TapMove", this, true);
+        } else {
+          this.hide(aEvent);
+        }
+        break;
+      case "TapUp":
+        window.removeEventListener("TapMove", this, true);
+        this.target = null;
+        this.deltaX = -1;
+        this.deltaY = -1;
+        break;
+      case "TapMove":
+        if (this.target) {
+          this.target.left = aEvent.clientX - this.deltaX;
+          this.target.top = aEvent.clientY - this.deltaY;
+          let rect = this.target.getBoundingClientRect();
+          let data = this.target == this._start ? { x: rect.right, y: rect.top, type: "start" } : { x: rect.left, y: rect.top, type: "end" };
+          let pos = this.popupState.target.transformClientToBrowser(data.x || 0, data.y || 0);
+          let json = {
+            type: data.type,
+            x: pos.x,
+            y: pos.y
+          };
+          this.popupState.target.messageManager.sendAsyncMessage("Browser:SelectionMove", json);
+        }
+        break;
+      case "resize":
+      case "SizeChanged":
+      case "ZoomChanged":
+      case "URLChanged":
+      case "keypress":
+        this.hide(aEvent);
+        break;
+    }
+  },
+
+  receiveMessage: function sh_receiveMessage(aMessage) {
+    let json = aMessage.json;
+    switch (aMessage.name) {
+      case "Browser:SelectionRange": {
+        let pos = this.popupState.target.transformBrowserToClient(json.start.x || 0, json.start.y || 0);
+        this._start.left = pos.x - 32;
+        this._start.top = pos.y + this.deltaY;
+        this._start.hidden = false;
+
+        pos = this.popupState.target.transformBrowserToClient(json.end.x || 0, json.end.y || 0);
+        this._end.left = pos.x;
+        this._end.top = pos.y;
+        this._end.hidden = false;
+        break;
+      }
+
+      case "Browser:SelectionCopied": {
+        messageManager.removeMessageListener("Browser:SelectionCopied", this);
+        if (json.succeeded) {
+          let toaster = Cc["@mozilla.org/toaster-alerts-service;1"].getService(Ci.nsIAlertsService);
+          toaster.showAlertNotification(null, Strings.browser.GetStringFromName("selectionHelper.textCopied"), "", false, "", null);
+        }
+        break;
+      }
+    }
+  }
+};
new file mode 100644
--- /dev/null
+++ b/mobile/chrome/content/WebappsUI.js
@@ -0,0 +1,173 @@
+/* ***** BEGIN LICENSE BLOCK *****
+ * Version: MPL 1.1/GPL 2.0/LGPL 2.1
+ *
+ * The contents of this file are subject to the Mozilla Public License Version
+ * 1.1 (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ * http://www.mozilla.org/MPL/
+ *
+ * Software distributed under the License is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
+ * for the specific language governing rights and limitations under the
+ * License.
+ *
+ * The Original Code is Mozilla Mobile Browser.
+ *
+ * The Initial Developer of the Original Code is
+ * Mozilla Foundation.
+ * Portions created by the Initial Developer are Copyright (C) 2011
+ * the Initial Developer. All Rights Reserved.
+ *
+ * Contributor(s):
+ *   Fabrice Desré <fabrice@mozilla.com>
+ *
+ * Alternatively, the contents of this file may be used under the terms of
+ * either the GNU General Public License Version 2 or later (the "GPL"), or
+ * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
+ * in which case the provisions of the GPL or the LGPL are applicable instead
+ * of those above. If you wish to allow use of your version of this file only
+ * under the terms of either the GPL or the LGPL, and not to allow others to
+ * use your version of this file under the terms of the MPL, indicate your
+ * decision by deleting the provisions above and replace them with the notice
+ * and other provisions required by the GPL or the LGPL. If you do not delete
+ * the provisions above, a recipient may use your version of this file under
+ * the terms of any one of the MPL, the GPL or the LGPL.
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+var WebappsUI = {
+  _dialog: null,
+  _manifest: null,
+  _perms: [],
+
+  checkBox: function(aEvent) {
+    let elem = aEvent.originalTarget;
+    let perm = elem.getAttribute("perm");
+    if (this._manifest.capabilities && this._manifest.capabilities.indexOf(perm) != -1) {
+      if (elem.checked) {
+        elem.classList.remove("webapps-noperm");
+        elem.classList.add("webapps-perm");
+      } else {
+        elem.classList.remove("webapps-perm");
+        elem.classList.add("webapps-noperm");
+      }
+    }
+  },
+
+  show: function show(aManifest) {
+    if (!aManifest) {
+      // Try every way to get an icon
+      let browser = Browser.selectedBrowser;
+      let icon = browser.appIcon.href;
+      if (!icon)
+        icon = browser.mIconURL;
+      if (!icon)
+        icon = gFaviconService.getFaviconImageForPage(browser.currentURI).spec;
+
+      // Create a simple manifest
+      aManifest = {
+        uri: browser.currentURI.spec,
+        name: browser.contentTitle,
+        icon: icon,
+        capabilities: [],
+      };
+    }
+
+    this._manifest = aManifest;
+    this._dialog = importDialog(window, "chrome://browser/content/webapps.xul", null);
+
+    if (aManifest.name)
+      document.getElementById("webapps-title").value = aManifest.name;
+    if (aManifest.icon)
+      document.getElementById("webapps-icon").src = aManifest.icon;
+
+    let uri = Services.io.newURI(aManifest.uri, null, null);
+
+    let perms = [["offline", "offline-app"], ["geoloc", "geo"], ["notifications", "desktop-notification"]];
+    let self = this;
+    perms.forEach(function(tuple) {
+      let elem = document.getElementById("webapps-" + tuple[0] + "-checkbox");
+      let currentPerm = Services.perms.testExactPermission(uri, tuple[1]);
+      self._perms[tuple[1]] = (currentPerm == Ci.nsIPermissionManager.ALLOW_ACTION);
+      if ((aManifest.capabilities && (aManifest.capabilities.indexOf(tuple[1]) != -1)) || (currentPerm == Ci.nsIPermissionManager.ALLOW_ACTION))
+        elem.checked = true;
+      else
+        elem.checked = (currentPerm == Ci.nsIPermissionManager.ALLOW_ACTION);
+      elem.classList.remove("webapps-noperm");
+      elem.classList.add("webapps-perm");
+    });
+
+    BrowserUI.pushPopup(this, this._dialog);
+
+    // Force a modal dialog
+    this._dialog.waitForClose();
+  },
+
+  hide: function hide() {
+    this._dialog.close();
+    this._dialog = null;
+    BrowserUI.popPopup(this);
+  },
+
+  _updatePermission: function updatePermission(aId, aPerm) {
+    try {
+      let uri = Services.io.newURI(this._manifest.uri, null, null);
+      let currentState = document.getElementById(aId).checked;
+      if (currentState != this._perms[aPerm])
+        Services.perms.add(uri, aPerm, currentState ? Ci.nsIPermissionManager.ALLOW_ACTION : Ci.nsIPermissionManager.DENY_ACTION);
+    } catch(e) {
+      Cu.reportError(e);
+    }
+  },
+
+  launch: function launch() {
+    let title = document.getElementById("webapps-title").value;
+    if (!title)
+      return;
+
+    this._updatePermission("webapps-offline-checkbox", "offline-app");
+    this._updatePermission("webapps-geoloc-checkbox", "geo");
+    this._updatePermission("webapps-notifications-checkbox", "desktop-notification");
+
+    this.hide();
+    this.install(this._manifest.uri, title, this._manifest.icon);
+  },
+
+  updateWebappsInstall: function updateWebappsInstall(aNode) {
+    if (document.getElementById("main-window").hasAttribute("webapp"))
+      return false;
+
+    let browser = Browser.selectedBrowser;
+
+    let webapp = Cc["@mozilla.org/webapps/support;1"].getService(Ci.nsIWebappsSupport);
+    return !(webapp && webapp.isApplicationInstalled(browser.currentURI.spec));
+  },
+
+  install: function(aURI, aTitle, aIcon) {
+    const kIconSize = 64;
+
+    let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
+    canvas.setAttribute("style", "display: none");
+
+    let self = this;
+    let image = new Image();
+    image.onload = function() {
+      canvas.width = canvas.height = kIconSize; // clears the canvas
+      let ctx = canvas.getContext("2d");
+      ctx.drawImage(image, 0, 0, kIconSize, kIconSize);
+      let data = canvas.toDataURL("image/png", "");
+      canvas = null;
+      try {
+        let webapp = Cc["@mozilla.org/webapps/support;1"].getService(Ci.nsIWebappsSupport);
+        webapp.installApplication(aTitle, aURI, aIcon, data);
+      } catch(e) {
+        Cu.reportError(e);
+      }
+    }
+    image.onerror = function() {
+      // can't load the icon (bad URI) : fallback to the default one from chrome
+      self.install(aURI, aTitle, "chrome://browser/skin/images/favicon-default-30.png");
+    }
+    image.src = aIcon;
+  }
+};
--- a/mobile/chrome/content/browser-scripts.js
+++ b/mobile/chrome/content/browser-scripts.js
@@ -64,26 +64,22 @@ Cu.import("resource://gre/modules/Geomet
 XPCOMUtils.defineLazyGetter(this, "CommonUI", function() {
   let CommonUI = {};
   Services.scriptloader.loadSubScript("chrome://browser/content/common-ui.js", CommonUI);
   return CommonUI;
 });
 
 [
   ["FullScreenVideo"],
-  ["WebappsUI"],
   ["BadgeHandlers"],
   ["ContextHelper"],
-  ["SelectionHelper"],
   ["FormHelperUI"],
   ["FindHelperUI"],
   ["NewTabPopup"],
-  ["PageActions"],
   ["BrowserSearch"],
-  ["CharsetMenu"]
 ].forEach(function (aObject) {
   XPCOMUtils.defineLazyGetter(window, aObject, function() {
     return CommonUI[aObject];
   });
 });
 
 /**
  * Delay load some browser scripts
@@ -91,34 +87,38 @@ XPCOMUtils.defineLazyGetter(this, "Commo
 [
   ["AlertsHelper", "chrome://browser/content/AlertsHelper.js"],
   ["AnimatedZoom", "chrome://browser/content/AnimatedZoom.js"],
   ["AppMenu", "chrome://browser/content/AppMenu.js"],
   ["AwesomePanel", "chrome://browser/content/AwesomePanel.js"],
   ["AwesomeScreen", "chrome://browser/content/AwesomePanel.js"],
   ["BookmarkHelper", "chrome://browser/content/BookmarkHelper.js"],
   ["BookmarkPopup", "chrome://browser/content/BookmarkPopup.js"],
+  ["CharsetMenu", "chrome://browser/content/CharsetMenu.js"],
   ["CommandUpdater", "chrome://browser/content/commandUtil.js"],
   ["ContextCommands", "chrome://browser/content/ContextCommands.js"],
   ["ConsoleView", "chrome://browser/content/console.js"],
   ["DownloadsView", "chrome://browser/content/downloads.js"],
   ["ExtensionsView", "chrome://browser/content/extensions.js"],
   ["MenuListHelperUI", "chrome://browser/content/MenuListHelperUI.js"],
   ["OfflineApps", "chrome://browser/content/OfflineApps.js"],
   ["IndexedDB", "chrome://browser/content/IndexedDB.js"],
+  ["PageActions", "chrome://browser/content/PageActions.js"],
   ["PreferencesView", "chrome://browser/content/preferences.js"],
   ["Sanitizer", "chrome://browser/content/sanitize.js"],
   ["SelectHelperUI", "chrome://browser/content/SelectHelperUI.js"],
+  ["SelectionHelper", "chrome://browser/content/SelectionHelper.js"],
   ["ContentPopupHelper", "chrome://browser/content/ContentPopupHelper.js"],
   ["SharingUI", "chrome://browser/content/SharingUI.js"],
   ["TabsPopup", "chrome://browser/content/TabsPopup.js"],
   ["MasterPasswordUI", "chrome://browser/content/MasterPasswordUI.js"],
 #ifdef MOZ_SERVICES_SYNC
   ["WeaveGlue", "chrome://browser/content/sync.js"],
 #endif
+  ["WebappsUI", "chrome://browser/content/WebappsUI.js"],
   ["SSLExceptions", "chrome://browser/content/exceptions.js"]
 ].forEach(function (aScript) {
   let [name, script] = aScript;
   XPCOMUtils.defineLazyGetter(window, name, function() {
     let sandbox = {};
     Services.scriptloader.loadSubScript(script, sandbox);
     return sandbox[name];
   });
--- a/mobile/chrome/content/browser-ui.js
+++ b/mobile/chrome/content/browser-ui.js
@@ -489,20 +489,18 @@ var BrowserUI = {
       Services.prefs.addObserver("browser.ui.layout.tablet", BrowserUI, false);
       Services.obs.addObserver(BrowserSearch, "browser-search-engine-modified", false);
       messageManager.addMessageListener("Browser:MozApplicationManifest", OfflineApps);
 
       // Init helpers
       BadgeHandlers.register(BrowserUI._edit.popup);
       FormHelperUI.init();
       FindHelperUI.init();
-      PageActions.init();
       FullScreenVideo.init();
       NewTabPopup.init();
-      CharsetMenu.init();
 
       // If some add-ons were disabled during during an application update, alert user
       let addonIDs = AddonManager.getStartupChanges("disabled");
       if (addonIDs.length > 0) {
         let disabledStrings = Strings.browser.GetStringFromName("alertAddonsDisabled");
         let label = PluralForm.get(addonIDs.length, disabledStrings).replace("#1", addonIDs.length);
         let image = "chrome://browser/skin/images/alert-addons-30.png";
 
--- a/mobile/chrome/content/common-ui.js
+++ b/mobile/chrome/content/common-ui.js
@@ -164,246 +164,16 @@ var BrowserSearch = {
 
   isPermanentSearchEngine: function isPermanentSearchEngine(aEngine) {
     return !BrowserSearch.engines.some(function(item) {
       return aEngine.title == item.name;
     });
   }
 };
 
-var PageActions = {
-  init: function init() {
-    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);
-    this.register("pageaction-search", BrowserSearch.updatePageSearchEngines, BrowserSearch);
-    this.register("pageaction-webapps-install", WebappsUI.updateWebappsInstall, WebappsUI);
-  },
-
-  handleEvent: function handleEvent(aEvent) {
-    switch (aEvent.type) {
-      case "click":
-        getIdentityHandler().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._handlers.push({id: aId, callback: aCallback, obj: aThisObj});
-  },
-
-  _handlers: [],
-
-  updateSiteMenu: function updateSiteMenu() {
-    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"],
-
-  _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.
-  },
-
-  savePageAsPDF: function saveAsPDF() {
-    let browser = Browser.selectedBrowser;
-    let fileName = ContentAreaUtils.getDefaultFileName(browser.contentTitle, browser.documentURI, null, null);
-    fileName = fileName.trim() + ".pdf";
-
-    let dm = Cc["@mozilla.org/download-manager;1"].getService(Ci.nsIDownloadManager);
-    let downloadsDir = dm.defaultDownloadsDirectory;
-
-#ifdef ANDROID
-    // Create the final destination file location
-    let file = downloadsDir.clone();
-    file.append(fileName);
-    file.createUnique(file.NORMAL_FILE_TYPE, 0666);
-#else
-    let picker = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
-    picker.init(window, Strings.browser.GetStringFromName("pageactions.saveas.pdf"), Ci.nsIFilePicker.modeSave);
-    picker.appendFilter("PDF", "*.pdf");
-    picker.defaultExtension = "pdf";
-
-    picker.defaultString = fileName;
-
-    picker.displayDirectory = downloadsDir;
-    let rv = picker.show();
-    if (rv == Ci.nsIFilePicker.returnCancel)
-      return;
-
-    let file = picker.file;
-#endif
-    fileName = file.leafName;
-
-    // We must manually add this to the download system
-    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)"
-    );
-
-    let current = browser.currentURI.spec;
-    stmt.params.name = fileName;
-    stmt.params.source = current;
-    stmt.params.target = Services.io.newFileURI(file).spec;
-    stmt.params.startTime = Date.now() * 1000;
-    stmt.params.endTime = Date.now() * 1000;
-    stmt.params.state = Ci.nsIDownloadManager.DOWNLOAD_NOTSTARTED;
-    stmt.params.referrer = current;
-    stmt.execute();
-    stmt.finalize();
-
-    let newItemId = db.lastInsertRowID;
-    let download = dm.getDownload(newItemId);
-    try {
-      DownloadsView.downloadStarted(download);
-    }
-    catch(e) {}
-    Services.obs.notifyObservers(download, "dl-start", null);
-
-#ifdef ANDROID
-    let tmpDir = Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties).get("TmpD", Ci.nsIFile);  
-    
-    file = tmpDir.clone();
-    file.append(fileName);
-
-#endif
-
-    let data = {
-      type: Ci.nsIPrintSettings.kOutputFormatPDF,
-      id: newItemId,
-      referrer: current,
-      filePath: file.path
-    };
-
-    Browser.selectedBrowser.messageManager.sendAsyncMessage("Browser:SaveAs", data);
-  },
-
-  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;
-
-    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");
-  }
-};
-
 var NewTabPopup = {
   _timeout: 0,
   _tabs: [],
 
   init: function init() {
     Elements.tabs.addEventListener("TabOpen", this, true);
   },
 
@@ -1301,182 +1071,16 @@ var ContextHelper = {
         aEvent.preventDefault();
         if (aEvent.keyCode != aEvent.DOM_VK_ESCAPE)
           this.hide();
         break;
     }
   }
 };
 
-var SelectionHelper = {
-  enabled: true,
-  popupState: null,
-  target: null,
-  deltaX: -1,
-  deltaY: -1,
-
-  get _start() {
-    delete this._start;
-    return this._start = document.getElementById("selectionhandle-start");
-  },
-
-  get _end() {
-    delete this._end;
-    return this._end = document.getElementById("selectionhandle-end");
-  },
-
-  showPopup: function sh_showPopup(aMessage) {
-    if (!this.enabled || aMessage.json.types.indexOf("content-text") == -1)
-      return false;
-
-    this.popupState = aMessage.json;
-    this.popupState.target = aMessage.target;
-
-    this._start.customDragger = {
-      isDraggable: function isDraggable(target, content) { return { x: true, y: false }; },
-      dragStart: function dragStart(cx, cy, target, scroller) {},
-      dragStop: function dragStop(dx, dy, scroller) { return false; },
-      dragMove: function dragMove(dx, dy, scroller) { return false; }
-    };
-
-    this._end.customDragger = {
-      isDraggable: function isDraggable(target, content) { return { x: true, y: false }; },
-      dragStart: function dragStart(cx, cy, target, scroller) {},
-      dragStop: function dragStop(dx, dy, scroller) { return false; },
-      dragMove: function dragMove(dx, dy, scroller) { return false; }
-    };
-
-    this._start.addEventListener("TapUp", this, true);
-    this._end.addEventListener("TapUp", this, true);
-
-    messageManager.addMessageListener("Browser:SelectionRange", this);
-    messageManager.addMessageListener("Browser:SelectionCopied", this);
-
-    this.popupState.target.messageManager.sendAsyncMessage("Browser:SelectionStart", { x: this.popupState.x, y: this.popupState.y });
-
-    // Hide the selection handles
-    window.addEventListener("TapDown", this, true);
-    window.addEventListener("resize", this, true);
-    window.addEventListener("keypress", this, true);
-    Elements.browsers.addEventListener("URLChanged", this, true);
-    Elements.browsers.addEventListener("SizeChanged", this, true);
-    Elements.browsers.addEventListener("ZoomChanged", this, true);
-
-    let event = document.createEvent("Events");
-    event.initEvent("CancelTouchSequence", true, false);
-    this.popupState.target.dispatchEvent(event);
-
-    return true;
-  },
-
-  hide: function sh_hide(aEvent) {
-    if (this._start.hidden)
-      return;
-
-    let pos = this.popupState.target.transformClientToBrowser(aEvent.clientX || 0, aEvent.clientY || 0);
-    let json = {
-      x: pos.x,
-      y: pos.y
-    };
-
-    try {
-      this.popupState.target.messageManager.sendAsyncMessage("Browser:SelectionEnd", json);
-    } catch (e) {
-      Cu.reportError(e);
-    }
-
-    this.popupState = null;
-
-    this._start.hidden = true;
-    this._end.hidden = true;
-
-    this._start.removeEventListener("TapUp", this, true);
-    this._end.removeEventListener("TapUp", this, true);
-
-    messageManager.removeMessageListener("Browser:SelectionRange", this);
-
-    window.removeEventListener("TapDown", this, true);
-    window.removeEventListener("resize", 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);
-  },
-
-  handleEvent: function handleEvent(aEvent) {
-    switch (aEvent.type) {
-      case "TapDown":
-        if (aEvent.target == this._start || aEvent.target == this._end) {
-          this.target = aEvent.target;
-          this.deltaX = (aEvent.clientX - this.target.left);
-          this.deltaY = (aEvent.clientY - this.target.top);
-          window.addEventListener("TapMove", this, true);
-        } else {
-          this.hide(aEvent);
-        }
-        break;
-      case "TapUp":
-        window.removeEventListener("TapMove", this, true);
-        this.target = null;
-        this.deltaX = -1;
-        this.deltaY = -1;
-        break;
-      case "TapMove":
-        if (this.target) {
-          this.target.left = aEvent.clientX - this.deltaX;
-          this.target.top = aEvent.clientY - this.deltaY;
-          let rect = this.target.getBoundingClientRect();
-          let data = this.target == this._start ? { x: rect.right, y: rect.top, type: "start" } : { x: rect.left, y: rect.top, type: "end" };
-          let pos = this.popupState.target.transformClientToBrowser(data.x || 0, data.y || 0);
-          let json = {
-            type: data.type,
-            x: pos.x,
-            y: pos.y
-          };
-          this.popupState.target.messageManager.sendAsyncMessage("Browser:SelectionMove", json);
-        }
-        break;
-      case "resize":
-      case "SizeChanged":
-      case "ZoomChanged":
-      case "URLChanged":
-      case "keypress":
-        this.hide(aEvent);
-        break;
-    }
-  },
-
-  receiveMessage: function sh_receiveMessage(aMessage) {
-    let json = aMessage.json;
-    switch (aMessage.name) {
-      case "Browser:SelectionRange": {
-        let pos = this.popupState.target.transformBrowserToClient(json.start.x || 0, json.start.y || 0);
-        this._start.left = pos.x - 32;
-        this._start.top = pos.y + this.deltaY;
-        this._start.hidden = false;
-
-        pos = this.popupState.target.transformBrowserToClient(json.end.x || 0, json.end.y || 0);
-        this._end.left = pos.x;
-        this._end.top = pos.y;
-        this._end.hidden = false;
-        break;
-      }
-
-      case "Browser:SelectionCopied": {
-        messageManager.removeMessageListener("Browser:SelectionCopied", this);
-        if (json.succeeded) {
-          let toaster = Cc["@mozilla.org/toaster-alerts-service;1"].getService(Ci.nsIAlertsService);
-          toaster.showAlertNotification(null, Strings.browser.GetStringFromName("selectionHelper.textCopied"), "", false, "", null);
-        }
-        break;
-      }
-    }
-  }
-};
-
 var BadgeHandlers = {
   _handlers: [
     {
       _lastUpdate: 0,
       _lastCount: 0,
       url: "https://mail.google.com/mail",
       updateBadge: function(aBadge) {
         // Use the cache if possible
@@ -1650,232 +1254,8 @@ var FullScreenVideo = {
     let pos = this.browser.transformClientToBrowser(aX, aY);
     this.browser.messageManager.sendAsyncMessage(aName, {
       x: pos.x,
       y: pos.y,
       messageId: null
     });
   }
 };
-
-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().documentCharsetInfo.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().documentCharsetInfo.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;
-    MenuListHelperUI.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);
-  }
-
-};
-
-var WebappsUI = {
-  _dialog: null,
-  _manifest: null,
-  _perms: [],
-  
-  checkBox: function(aEvent) {
-    let elem = aEvent.originalTarget;
-    let perm = elem.getAttribute("perm");
-    if (this._manifest.capabilities && this._manifest.capabilities.indexOf(perm) != -1) {
-      if (elem.checked) {
-        elem.classList.remove("webapps-noperm");
-        elem.classList.add("webapps-perm");
-      } else {
-        elem.classList.remove("webapps-perm");
-        elem.classList.add("webapps-noperm");
-      }
-    }
-  },
-
-  show: function show(aManifest) {
-    if (!aManifest) {
-      // Try every way to get an icon
-      let browser = Browser.selectedBrowser;
-      let icon = browser.appIcon.href;
-      if (!icon)
-        icon = browser.mIconURL;
-      if (!icon) 
-        icon = gFaviconService.getFaviconImageForPage(browser.currentURI).spec;
-
-      // Create a simple manifest
-      aManifest = {
-        uri: browser.currentURI.spec,
-        name: browser.contentTitle,
-        icon: icon,
-        capabilities: [],
-      };
-    }
-
-    this._manifest = aManifest;
-    this._dialog = importDialog(window, "chrome://browser/content/webapps.xul", null);
-
-    if (aManifest.name)
-      document.getElementById("webapps-title").value = aManifest.name;
-    if (aManifest.icon)
-      document.getElementById("webapps-icon").src = aManifest.icon;  
-
-    let uri = Services.io.newURI(aManifest.uri, null, null);
-
-    let perms = [["offline", "offline-app"], ["geoloc", "geo"], ["notifications", "desktop-notification"]];
-    let self = this;
-    perms.forEach(function(tuple) {
-      let elem = document.getElementById("webapps-" + tuple[0] + "-checkbox");
-      let currentPerm = Services.perms.testExactPermission(uri, tuple[1]);
-      self._perms[tuple[1]] = (currentPerm == Ci.nsIPermissionManager.ALLOW_ACTION);
-      if ((aManifest.capabilities && (aManifest.capabilities.indexOf(tuple[1]) != -1)) || (currentPerm == Ci.nsIPermissionManager.ALLOW_ACTION))
-        elem.checked = true;
-      else
-        elem.checked = (currentPerm == Ci.nsIPermissionManager.ALLOW_ACTION);
-      elem.classList.remove("webapps-noperm");
-      elem.classList.add("webapps-perm");
-    });
-
-    BrowserUI.pushPopup(this, this._dialog);
-
-    // Force a modal dialog
-    this._dialog.waitForClose();
-  },
-
-  hide: function hide() {
-    this._dialog.close();
-    this._dialog = null;
-    BrowserUI.popPopup(this);
-  },
-
-  _updatePermission: function updatePermission(aId, aPerm) {
-    try {
-      let uri = Services.io.newURI(this._manifest.uri, null, null);
-      let currentState = document.getElementById(aId).checked;
-      if (currentState != this._perms[aPerm])
-        Services.perms.add(uri, aPerm, currentState ? Ci.nsIPermissionManager.ALLOW_ACTION : Ci.nsIPermissionManager.DENY_ACTION);
-    } catch(e) {
-      Cu.reportError(e);
-    }
-  },
-  
-  launch: function launch() {
-    let title = document.getElementById("webapps-title").value;
-    if (!title)
-      return;
-
-    this._updatePermission("webapps-offline-checkbox", "offline-app");
-    this._updatePermission("webapps-geoloc-checkbox", "geo");
-    this._updatePermission("webapps-notifications-checkbox", "desktop-notification");
-
-    this.hide();
-    this.install(this._manifest.uri, title, this._manifest.icon);
-  },
-  
-  updateWebappsInstall: function updateWebappsInstall(aNode) {
-    if (document.getElementById("main-window").hasAttribute("webapp"))
-      return false;
-
-    let browser = Browser.selectedBrowser;
-
-    let webapp = Cc["@mozilla.org/webapps/support;1"].getService(Ci.nsIWebappsSupport);
-    return !(webapp && webapp.isApplicationInstalled(browser.currentURI.spec));
-  },
-  
-  install: function(aURI, aTitle, aIcon) {
-    const kIconSize = 64;
-    
-    let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
-    canvas.setAttribute("style", "display: none");
-
-    let self = this;
-    let image = new Image();
-    image.onload = function() {
-      canvas.width = canvas.height = kIconSize; // clears the canvas
-      let ctx = canvas.getContext("2d");
-      ctx.drawImage(image, 0, 0, kIconSize, kIconSize);
-      let data = canvas.toDataURL("image/png", "");
-      canvas = null;
-      try {
-        let webapp = Cc["@mozilla.org/webapps/support;1"].getService(Ci.nsIWebappsSupport);
-        webapp.installApplication(aTitle, aURI, aIcon, data);
-      } catch(e) {
-        Cu.reportError(e);
-      }
-    }
-    image.onerror = function() {
-      // can't load the icon (bad URI) : fallback to the default one from chrome
-      self.install(aURI, aTitle, "chrome://browser/skin/images/favicon-default-30.png");
-    }
-    image.src = aIcon;
-  }
-};
--- a/mobile/chrome/jar.mn
+++ b/mobile/chrome/jar.mn
@@ -18,22 +18,25 @@ chrome.jar:
 * content/browser-ui.js                (content/browser-ui.js)
 * content/browser-scripts.js           (content/browser-scripts.js)
 * content/common-ui.js                 (content/common-ui.js)
 * content/AlertsHelper.js              (content/AlertsHelper.js)
   content/AppMenu.js                   (content/AppMenu.js)
 * content/AwesomePanel.js              (content/AwesomePanel.js)
   content/BookmarkHelper.js            (content/BookmarkHelper.js)
   content/BookmarkPopup.js             (content/BookmarkPopup.js)
+  content/CharsetMenu.js               (content/CharsetMenu.js)
   content/ContentPopupHelper.js        (content/ContentPopupHelper.js)
 * content/ContextCommands.js           (content/ContextCommands.js)
   content/IndexedDB.js                 (content/IndexedDB.js)
   content/MenuListHelperUI.js          (content/MenuListHelperUI.js)
   content/OfflineApps.js               (content/OfflineApps.js)
+* content/PageActions.js               (content/PageActions.js)
   content/SelectHelperUI.js            (content/SelectHelperUI.js)
+  content/SelectionHelper.js           (content/SelectionHelper.js)
   content/SharingUI.js                 (content/SharingUI.js)
   content/TabsPopup.js                 (content/TabsPopup.js)
   content/MasterPasswordUI.js          (content/MasterPasswordUI.js)
 * content/content.js                   (content/content.js)
   content/commandUtil.js               (content/commandUtil.js)
 * content/bindings.xml                 (content/bindings.xml)
   content/tabs.xml                     (content/tabs.xml)
   content/bindings/checkbox.xml        (content/bindings/checkbox.xml)
@@ -61,16 +64,17 @@ chrome.jar:
   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/share.xul                    (content/share.xul)
   content/webapps.xul                  (content/webapps.xul)
+  content/WebappsUI.js                 (content/WebappsUI.js)
   content/masterPassword.xul           (content/masterPassword.xul)
   content/removeMasterPassword.xul     (content/removeMasterPassword.xul)
   content/AnimatedZoom.js              (content/AnimatedZoom.js)
 #ifdef MOZ_SERVICES_SYNC
   content/sync.js                      (content/sync.js)
 #endif
   content/LoginManagerChild.js         (content/LoginManagerChild.js)
   content/fullscreen-video.js          (content/fullscreen-video.js)