chrome/content/bindings/browser.xml
author Matt Brubeck <mbrubeck@mozilla.com>
Wed, 02 Feb 2011 16:25:42 -0800
changeset 2735 1165fcc932427cf7ba66dffd5c800753a4dd4c3e
parent 2734 ecb8351218370e1543e8cf40c287c37a6099987d
child 2736 43e6a7f68527d6926f3a97005bed30e569022c52
permissions -rw-r--r--
Bug 628799 (followup) - Fix the ZoomChanged event used by Tzoom tests [r=stechz]

<?xml version="1.0"?>

<!-- ***** 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 this file as it was released on March 28, 2001.
   -
   - The Initial Developer of the Original Code is
   - Peter Annema.
   - Portions created by the Initial Developer are Copyright (C) 2001
   - the Initial Developer. All Rights Reserved.
   -
   - Contributor(s):
   -   Peter Annema <disttsc@bart.nl> (Original Author of <browser>)
   -   Peter Parente <parente@cs.unc.edu>
   -   Christopher Thomas <cst@yecc.com>
   -   Michael Ventnor <m.ventnor@gmail.com>
   -   Arpad Borsos <arpad.borsos@googlemail.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 ***** -->

<!DOCTYPE bindings [
  <!ENTITY % findBarDTD SYSTEM "chrome://global/locale/findbar.dtd" >
  %findBarDTD;
]>

<bindings id="remoteBrowserBindings"
          xmlns="http://www.mozilla.org/xbl"
          xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">

  <binding id="local-browser" extends="chrome://global/content/bindings/browser.xml#browser">
    <implementation type="application/javascript" implements="nsIAccessibleProvider, nsIObserver, nsIDOMEventListener, nsIFrameMessageListener">
      <field name="_securityUI">null</field>
      <property name="securityUI">
        <getter><![CDATA[
          return this._securityUI || {};
        ]]></getter>
        <setter><![CDATA[
          this._securityUI = val;
        ]]></setter>
      </property>

      <field name="_searchEngines">[]</field>
      <property name="searchEngines"
                onget="return this._searchEngines"
                readonly="true"/>

      <field name="_documentURI">null</field>
      <property name="documentURI"
                onget="return this._documentURI ? this._ios.newURI(this._documentURI, null, null) : null"
                readonly="true"/>

      <field name="contentWindowId">null</field>

      <property name="messageManager"
                onget="return this._frameLoader.messageManager;"
                readonly="true"/>

      <field name="_contentTitle">null</field>

      <field name="_ios">
         Components.classes["@mozilla.org/network/io-service;1"].getService(Components.interfaces.nsIIOService);
      </field>

      <field name="_messageListenerLocal"><![CDATA[
        ({
          self: this,
          receiveMessage: function receiveMessage(aMessage) {
            let self = this.self;
            let json = aMessage.json;

            switch (aMessage.name) {
              case "DOMPopupBlocked":
                self.onPopupBlocked(aMessage);
                break;

              case "pageshow":
                self.onPageShow(aMessage);

                if (!self.mIconURL && self._documentURI) {
                  let iconURL = null;
                  if (self.shouldLoadFavicon()) {
                    // Use documentURI in the favicon construction so that we
                    // do the right thing with about:-style error pages.  Bug 515188
                    iconURL = self.documentURI.prePath + "/favicon.ico";
                  }
                  self.loadFavicon(iconURL, null);
                }
                break;

              case "pagehide":
                self.onPageHide(aMessage);
                break;

              case "DOMTitleChanged":
                self._contentTitle = aMessage.json.title;
                break;

              case "DOMLinkAdded":
                let link = aMessage.json;
                // ignore results from subdocuments
                if (link.windowId != self.contentWindowId)
                  return;

                let linkType = self._getLinkType(link);
                switch(linkType) {
                  case "icon":
                    self.loadFavicon(link.href, link.charset);
                    break;
                  case "search":
                    self._searchEngines.push({ title: link.title, href: link.href });
                    break;
                }
                break;

              case "MozScrolledAreaChanged":
                self._contentDocumentWidth = aMessage.json.width;
                self._contentDocumentHeight = aMessage.json.height;

                // Recalculate whether the visible area is actually in bounds
                let view = self.getRootView();
                view.scrollBy(0, 0);

                // XXX Actually this fire a SetCacheViewport event as well as the
                // scrolledAreaChanged handler in content/chrome/browser.js. So
                // we ended up doing twice the work for the same area
                view._updateCacheViewport();
                break;
            }
          }
        })
      ]]></field>

      <method name="loadFavicon">
        <parameter name="aURL"/>
        <parameter name="aCharset"/>
        <body><![CDATA[
            try { // newURI call is throwing for chrome URI
              let iconURI = this._ios.newURI(aURL, aCharset, null);
              if (gFaviconService.isFailedFavicon(iconURI))
                return;

              gFaviconService.setAndLoadFaviconForPage(this.currentURI, iconURI, true);
              this.mIconURL = iconURI.spec;
            } catch (e) {
              this.mIconURL = null;
            }
        ]]></body>
      </method>

      <method name="shouldLoadFavicon">
        <body><![CDATA[
          let docURI = this.documentURI;
          return (docURI && ("schemeIs" in docURI) &&
                  (docURI.schemeIs("http") || docURI.schemeIs("https")));
        ]]></body>
      </method>

      <method name="_getLinkType">
        <parameter name="aLink" />
        <body><![CDATA[
          let type = "";
          if (/\bicon\b/i(aLink.rel)) {
            type = "icon";
          }
          else if (/\bsearch\b/i(aLink.rel) && aLink.type && aLink.title) {
            let linkType = aLink.type.replace(/^\s+|\s*(?:;.*)?$/g, "").toLowerCase();
            if (linkType == "application/opensearchdescription+xml" && /^(?:https?|ftp):/i.test(aLink.href)) {
              type = "search";
            }
          }

          return type;
        ]]></body>
      </method>

      <field name="_webProgress"><![CDATA[
        ({
          _browser: this,

          _init: function() {
            this._browser.messageManager.addMessageListener("Content:StateChange", this);
            this._browser.messageManager.addMessageListener("Content:LocationChange", this);
            this._browser.messageManager.addMessageListener("Content:SecurityChange", this);
          },

          receiveMessage: function(aMessage) {
            let json = aMessage.json;
            switch (aMessage.name) {
              case "Content:StateChange":
                this._browser.updateWindowId(json.contentWindowId);
                break;

              case "Content:LocationChange":
                try {
                  let locationURI = this._browser._ios.newURI(json.location, null, null);
                  this._browser._webNavigation._currentURI = locationURI;
                  this._browser._webNavigation.canGoBack = json.canGoBack;
                  this._browser._webNavigation.canGoForward = json.canGoForward;
                } catch(e) {}

                if (this._browser.updateWindowId(json.contentWindowId)) {
                  this._browser._documentURI = json.documentURI;
                  this._browser._searchEngines = [];
                }
                break;

              case "Content:SecurityChange":
                let serhelper = Components.classes["@mozilla.org/network/serialization-helper;1"]
                                .getService(Components.interfaces.nsISerializationHelper);
                let SSLStatus = json.SSLStatusAsString ? serhelper.deserializeObject(json.SSLStatusAsString) : null;
                if (SSLStatus) {
                  SSLStatus.QueryInterface(Components.interfaces.nsISSLStatus);
                  if (SSLStatus) {
                    // We must check the Extended Validation (EV) state here, on the chrome
                    // process, because NSS is needed for that determination.
                    let ii = SSLStatus.serverCert.QueryInterface(Components.interfaces.nsIIdentityInfo);
                    if (ii && ii.isExtendedValidation)
                      json.state |= Components.interfaces.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL;
                  }
                }

                let data = this._getIdentityData(SSLStatus);
                this._browser._securityUI = {
                  SSLStatus: SSLStatus ? {serverCert: data} : null,
                  state: json.state
                }
                this._browser.updateWindowId(json.contentWindowId);
                break;
            }
          },

          /**
           * Helper to parse out the important parts of the SSL cert for use in constructing
           * identity UI strings
           */
          _getIdentityData: function(status) {
            let result = {};

            if (status) {
              let cert = status.serverCert;

              // Human readable name of Subject
              result.subjectOrg = cert.organization;

              // SubjectName fields, broken up for individual access
              if (cert.subjectName) {
                result.subjectNameFields = {};
                cert.subjectName.split(",").forEach(function(v) {
                  var field = v.split("=");
                  if (field[1])
                    this[field[0]] = field[1];
                }, result.subjectNameFields);

                // Call out city, state, and country specifically
                result.city = result.subjectNameFields.L;
                result.state = result.subjectNameFields.ST;
                result.country = result.subjectNameFields.C;
              }

              // Human readable name of Certificate Authority
              result.caOrg =  cert.issuerOrganization || cert.issuerCommonName;

              if (!this._overrideService)
                this._overrideService = Components.classes["@mozilla.org/security/certoverride;1"]
                                        .getService(Components.interfaces.nsICertOverrideService);

              // Check whether this site is a security exception.
              let currentURI = this._browser._webNavigation._currentURI;
              result.isException = !!this._overrideService.hasMatchingOverride(currentURI.asciiHost, currentURI.port, cert, {}, {});
            }

            return result;
          }
        })
      ]]></field>

      <property name="webProgress"
                readonly="true"
                onget="return null"/>

      <method name="onPageShow">
        <parameter name="aMessage"/>
        <body>
          <![CDATA[
            this.attachFormFill();
            if (this.pageReport) {
              var json = aMessage.json;
              var i = 0;
              while (i < this.pageReport.length) {
                // Filter out irrelevant reports.
                if (this.pageReport[i].requestingWindowId == json.windowId)
                  i++;
                else
                  this.pageReport.splice(i, 1);
              }
              if (this.pageReport.length == 0) {
                this.pageReport = null;
                this.updatePageReport();
              }
            }
         ]]>
        </body>
      </method>

      <method name="onPageHide">
        <parameter name="aMessage"/>
        <body>
          <![CDATA[
            if (this.pageReport) {
              this.pageReport = null;
              this.updatePageReport();
            }
            // Delete the feeds cache if we're hiding the topmost page
            // (as opposed to one of its iframes).
            if (this.feeds && aMessage.target == this)
              this.feeds = null;

            this.scale = 1;
         ]]>
        </body>
      </method>

      <method name="onPopupBlocked">
        <parameter name="aMessage"/>
        <body>
          <![CDATA[
            if (!this.pageReport) {
              this.pageReport = [];
            }

            let json = aMessage.json;
            // XXX Replacing requestingWindow && requestingDocument affects
            // http://mxr.mozilla.org/mozilla-central/source/browser/base/content/browser.js#500
            var obj = {
              requestingWindowId: json.windowId,
              popupWindowURI: this._ios.newURI(json.popupWindowURI.spec, json.popupWindowURI.charset, null),
              popupWindowFeatures: json.popupWindowFeatures,
              popupWindowName: json.popupWindowName
            };

            this.pageReport.push(obj);
            this.pageReport.reported = false;
            this.updatePageReport();
          ]]>
        </body>
      </method>

      <field name="_frameLoader">null</field>
      <field name="_contentViewManager">null</field>

      <!-- Dimensions of content window -->
      <property name="contentWindowWidth"
                onget="return this._contentWindowWidth;"
                readonly="true"/>
      <property name="contentWindowHeight"
                onget="return this._contentWindowHeight;"
                readonly="true"/>

      <!-- Dimensions of content document -->
      <field name="_contentDocumentWidth">0</field>
      <field name="_contentDocumentHeight">0</field>
      <property name="contentDocumentWidth"
                onget="return this._contentDocumentWidth;"
                readonly="true"/>
      <property name="contentDocumentHeight"
                onget="return this._contentDocumentHeight;"
                readonly="true"/>

      <!-- The ratio of device pixels to CSS pixels -->
      <property name="scale"
                onget="return 1;"
                onset="return 1;"/>

      <!-- These counters are used to update the cached viewport after they reach a certain
           threshold when scrolling -->
      <field name="_cacheRatioWidth">1</field>
      <field name="_cacheRatioHeight">1</field>

      <!-- Used in remote tabs only. -->
      <method name="_updateCSSViewport">
        <body/>
      </method>

      <!-- Sets size of CSS viewport, which affects how page is layout. -->
      <method name="setWindowSize">
        <parameter name="width"/>
        <parameter name="height"/>
        <body>
          <![CDATA[
            this._contentWindowWidth = width;
            this._contentWindowHeight = height;
            this.messageManager.sendAsyncMessage("Content:SetWindowSize", {
              width: width,
              height: height
            });
          ]]>
        </body>
      </method>

      <method name="getRootView">
        <body>
          <![CDATA[
            return this._contentView;
          ]]>
        </body>
      </method>

      <field name="_contentViewPrototype"><![CDATA[
        ({
          _scrollbox: null,

          init: function(aElement) {
            this._scrollbox = aElement.scrollBoxObject;
          },

          isRoot: function() {
            return false;
          },

          scrollBy: function(x, y) {
            this._scrollbox.scrollBy(x,y);
          },

          scrollTo: function(x, y) {
            this._scrollbox.scrollTo(x,y);
          },

          getPosition: function() {
            let x = {}, y = {};
            this._scrollbox.getPosition(x, y);
            return { x: x.value, y: y.value };
          }
        })
        ]]>
      </field>

      <method name="getViewAt">
        <parameter name="x"/>
        <parameter name="y"/>
        <body>
          <![CDATA[
            let cwu = this.contentDocument.defaultView.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
                                                      .getInterface(Components.interfaces.nsIDOMWindowUtils);
            let elt = cwu.elementFromPoint(x, y, false, false);

            while (elt && !elt.scrollBoxObject)
              elt = elt.parentNode;
 
            if (!elt)
              return this._contentView;

            let cv = Object.create(this._contentViewPrototype);
            cv.init(elt);
            return cv;
          ]]>
        </body>
      </method>

      <field name="_contentView"><![CDATA[
        ({
          self: this,

          _updateCacheViewport: function() {
          },

          isRoot: function() {
            return true;
          },

          scrollBy: function(x, y) {
            let self = this.self;
            let position = this.getPosition();
            x = Math.floor(Math.max(0, Math.min(self.contentDocumentWidth,  position.x + x)) - position.x);
            y = Math.floor(Math.max(0, Math.min(self.contentDocumentHeight, position.y + y)) - position.y);
            self.contentWindow.scrollBy(x, y);
          },

          scrollTo: function(x, y) {
            let self = this.self;
            x = Math.floor(Math.max(0, Math.min(self.contentDocumentWidth,  x)));
            y = Math.floor(Math.max(0, Math.min(self.contentDocumentHeight, y)));
            self.contentWindow.scrollTo(x, y);
          },

          getPosition: function() {
            let self = this.self;
            let scrollX = {}, scrollY = {};
            let cwu = self.contentWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor).
                      getInterface(Components.interfaces.nsIDOMWindowUtils);
            cwu.getScrollXY(false, scrollX, scrollY);
            return { x: scrollX.value, y: scrollY.value };
          },

          toString: function() {
            return "[View Local]";
          }
        })
        ]]>
      </field>

      <!-- Change client coordinates in device pixels to page-relative ones in CSS px. -->
      <method name="transformClientToBrowser">
        <parameter name="clientX"/>
        <parameter name="clientY"/>
        <body>
          <![CDATA[
            let bcr = this.getBoundingClientRect();
            let view = this.getRootView();
            let scroll = this.getRootView().getPosition();
            return { x: (clientX + scroll.x - bcr.left) / this.scale,
                     y: (clientY + scroll.y - bcr.top) / this.scale };
          ]]>
        </body>
      </method>

      <constructor>
        <![CDATA[
          this._frameLoader = this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner).frameLoader;
          this._contentViewManager = this._frameLoader.QueryInterface(Components.interfaces.nsIContentViewManager);

          let prefService = Components.classes["@mozilla.org/preferences-service;1"]
                                              .getService(Components.interfaces.nsIPrefService)
                                              .QueryInterface(Components.interfaces.nsIPrefBranch2);

          this._cacheRatioWidth = Math.max(1, prefService.getIntPref("toolkit.browser.cacheRatioWidth") / 1000);
          this._cacheRatioHeight = Math.max(1, prefService.getIntPref("toolkit.browser.cacheRatioHeight") / 1000);

          if (this._contentViewPrototype)
            this._contentViewPrototype.kDieTime = prefService.getIntPref("toolkit.browser.contentViewExpire");

          this.messageManager.loadFrameScript("chrome://browser/content/bindings/browser.js", true);
          this.messageManager.addMessageListener("DOMTitleChanged", this._messageListenerLocal);
          this.messageManager.addMessageListener("DOMLinkAdded", this._messageListenerLocal);
          this.messageManager.addMessageListener("pageshow", this._messageListenerLocal);
          this.messageManager.addMessageListener("pagehide", this._messageListenerLocal);
          this.messageManager.addMessageListener("DOMPopupBlocked", this._messageListenerLocal);
          this.messageManager.addMessageListener("MozScrolledAreaChanged", this._messageListenerLocal);

          this._webProgress._init();
        ]]>
      </constructor>

      <method name="updateWindowId">
         <parameter name="aNewId"/>
         <body><![CDATA[
            if (this.contentWindowId != aNewId) {
               this.contentWindowId = aNewId;
               return true;
            }
            return false;
         ]]></body>
      </method>

      <field name="_active">false</field>
      <property name="active" onget="return this._active;">
        <setter><![CDATA[
          // Do not change displayport on local tabs!
          this._active = val;
        ]]></setter>
      </property>

      <!-- Transform the viewport without updating the displayport. -->
      <method name="fuzzyZoom">
        <parameter name="scale"/>
        <parameter name="x"/>
        <parameter name="y"/>
        <body><![CDATA[
          this.getRootView().scrollTo(x, y);
        ]]></body>
      </method>

      <!-- After fuzzy zoom, sync the displayport with the new viewport. -->
      <method name="finishFuzzyZoom">
        <body><![CDATA[
          return;
        ]]></body>
      </method>

    </implementation>
  </binding>

  <binding id="remote-browser" extends="#local-browser">
    <implementation type="application/javascript" implements="nsIAccessibleProvider, nsIObserver, nsIDOMEventListener, nsIFrameMessageListener">
      <property name="accessibleType" readonly="true">
        <getter>
          <![CDATA[
            throw "accessibleType: Supports Remote?";
          ]]>
        </getter>
      </property>

      <property name="autoscrollEnabled">
        <getter>
          <![CDATA[
            throw "autoscrollEnabled: Supports Remote?";
          ]]>
        </getter>
      </property>

      <property name="docShell"
                onget="return null"
                readonly="true"/>

      <field name="_contentTitle">null</field>

      <property name="contentTitle"
                onget="return this._contentTitle;"
                readonly="true"/>

      <field name="_webNavigation"><![CDATA[
        ({
          LOAD_FLAGS_MASK: 65535,
          LOAD_FLAGS_NONE: 0,
          LOAD_FLAGS_IS_REFRESH: 16,
          LOAD_FLAGS_IS_LINK: 32,
          LOAD_FLAGS_BYPASS_HISTORY: 64,
          LOAD_FLAGS_REPLACE_HISTORY: 128,
          LOAD_FLAGS_BYPASS_CACHE: 256,
          LOAD_FLAGS_BYPASS_PROXY: 512,
          LOAD_FLAGS_CHARSET_CHANGE: 1024,
          LOAD_FLAGS_STOP_CONTENT: 2048,
          LOAD_FLAGS_FROM_EXTERNAL: 4096,
          LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP: 8192,
          LOAD_FLAGS_FIRST_LOAD: 16384,
          LOAD_FLAGS_ALLOW_POPUPS: 32768,
          LOAD_FLAGS_BYPASS_CLASSIFIER: 65536,
          LOAD_FLAGS_FORCE_ALLOW_COOKIES: 131072,

          STOP_NETWORK: 1,
          STOP_CONTENT: 2,
          STOP_ALL: 3,

          canGoBack: false,
          canGoForward: false,
          goBack: function() { this._sendMessage("WebNavigation:GoBack", {}); },
          goForward: function() { this._sendMessage("WebNavigation:GoForward", {}); },
          gotoIndex: function(aIndex) { this._sendMessage("WebNavigation:GotoIndex", {index: aIndex}); },
          loadURI: function(aURI, aLoadFlags, aReferrer, aPostData, aHeaders) {
            this._browser.userTypedValue = aURI;
            this._browser._contentTitle = "";
            this._sendMessage("WebNavigation:LoadURI", {uri: aURI, flags: aLoadFlags});
          },
          reload: function(aReloadFlags) { this._sendMessage("WebNavigation:Reload", {flags: aReloadFlags}); },
          stop: function(aStopFlags) { this._sendMessage("WebNavigation:Stop", {flags: aStopFlags}); },
          get document() { Components.utils.reportError("contentDocument is not available"); return null; },
          get currentURI() {
            if (!this._currentURI)
               this._currentURI = this._browser._ios.newURI("about:blank", null, null);

            return this._currentURI;
          },
          set currentURI(aURI) { this.loadURI(aURI.spec, null, null, null); },
          referringURI: null,
          get sessionHistory() { return null; },
          set sessionHistory(aValue) { },

          _currentURI: null,
          _browser: this,
          _sendMessage: function(aMessage, aData) {
            try {
              this._browser.messageManager.sendAsyncMessage(aMessage, aData);
            }
            catch (e) {
              Components.utils.reportError(e);
            }
         },

         QueryInterface: function(aIID) {
           if (aIID.equals(Components.interfaces.nsIWebNavigation) || aIID.equals(Components.interfaces.nsISupports))
             return this;
           throw Components.results.NS_NOINTERFACE;
         }
        })
      ]]></field>

      <property name="webNavigation"
                onget="return this._webNavigation;"
                readonly="true"/>

      <property name="contentWindow"
                readonly="true"
                onget="return null"/>

      <property name="sessionHistory"
                onget="return null"
                readonly="true"/>

      <property name="markupDocumentViewer"
                onget="return null"
                readonly="true"/>

      <property name="contentViewerEdit"
                onget="return null"
                readonly="true"/>

      <property name="contentViewerFile"
                onget="return null"
                readonly="true"/>

      <property name="documentCharsetInfo"
                onget="return null"
                readonly="true"/>

      <constructor>
        <![CDATA[
          this.messageManager.addMessageListener("scroll", this._messageListenerRemote);
        ]]>
      </constructor>

      <field name="scrollSync">true</field>

      <field name="_messageListenerRemote"><![CDATA[
        ({
          self: this,
          receiveMessage: function receiveMessage(aMessage) {
            let self = this.self;
            let json = aMessage.json;

            switch (aMessage.name) {
              case "scroll":
                if (!self.scrollSync)
                  return;

                // Use floor so that we always guarantee top-left corner of content is visible.
                let view = self.getRootView();
                view.scrollTo(Math.floor(json.x * self.scale), Math.floor(json.y * self.scale));
                break;
           }
         }
       })
      ]]></field>

      <!-- Keep a store of temporary content views. -->
      <field name="_contentViews">({})</field>

      <!-- There is a point before a page has loaded where a root content view
           may not exist. We use this so that we don't have to worry about doing
           an if check every time we want to scroll. -->
      <field name="_contentNoop"><![CDATA[
        ({
          _updateCacheViewport: function() {},
          _getViewportSize: function() {},

          isRoot: function() {
            return true;
          },

          _scale: 1,
          _setScale: function(scale) {},
          scrollBy: function(x, y) {},
          scrollTo: function(x, y) {},
          getPosition: function() {
            return { x: 0, y: 0 };
          }
        })
      ]]></field>

      <field name="_contentViewPrototype"><![CDATA[
        ({
          self: this,
          _id: null,
          _contentView: null,
          _timeout: null,
          _pixelsPannedSinceRefresh: { x: 0, y: 0 },
          _lastPanTime: 0,

          kDieTime: 3000,

          /** 
           * Die if we haven't panned in a while.
           *
           * Since we keep a map of active content views, we need to regularly
           * check if they are necessary so that every single thing the user
           * pans is not kept in memory forever.
           */
          _dieIfOld: function() {
            if (Date.now() - this._lastPanTime >= 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 && 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) * contentHeight / contentWidth;
              cacheWidth = contentWidth;
            } else if (contentHeight < cacheHeight) {
              cacheWidth += (cacheHeight - contentHeight) * contentWidth / contentHeight;
              cacheHeight = contentHeight;
            }

            return { width: cacheWidth, height: cacheHeight };
          },

          /**
           * 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() {
            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.
            let bounds = new Rect(0, 0, contentSize.width, contentSize.height);
            let displayport = new Rect(cacheX, cacheY, cacheSize.width, cacheSize.height);
            displayport.translateInside(bounds);

            self.messageManager.sendAsyncMessage("Content:SetCacheViewport", {
              scrollX: contentView.scrollX / this._scale,
              scrollY: contentView.scrollY / this._scale,
              x: displayport.x / this._scale,
              y: displayport.y / this._scale,
              w: displayport.width / this._scale,
              h: displayport.height / this._scale,
              scale: this._scale,
              id: contentView.id
            });

            this._pixelsPannedSinceRefresh.x = 0;
            this._pixelsPannedSinceRefresh.y = 0;
          },

          _getContentSize: function() {
            let self = this.self;
            if (this.isRoot()) {
              // XXX Bug 626792 means contentWidth and contentHeight aren't always
              // updated immediately. This makes the displayport go haywire so
              // use contentDocument properties.
              return { width: self._contentDocumentWidth * this._scale,
                       height: self._contentDocumentHeight * this._scale };
            } else {
              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) {
            // 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;

            x = Math.floor(Math.max(0, Math.min(scrollRangeX, 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 };
          }
        })
        ]]>
      </field>

      <!-- Transform the viewport without updating the displayport. -->
      <method name="fuzzyZoom">
        <parameter name="scale"/>
        <parameter name="x"/>
        <parameter name="y"/>
        <body><![CDATA[
          let rootView = this.getRootView();
          rootView._setScale(scale);
          rootView._contentView.scrollTo(x, y);
        ]]></body>
      </method>

      <!-- After fuzzy zoom, sync the displayport with the new viewport. -->
      <method name="finishFuzzyZoom">
        <body><![CDATA[
          this.getRootView()._updateCacheViewport();

          let event = document.createEvent("Events");
          event.initEvent("ZoomChanged", true, false);
          this.dispatchEvent(event);
        ]]></body>
      </method>

      <!-- The ratio of CSS pixels to device pixels. -->
      <property name="scale">
        <getter><![CDATA[
          return this.getRootView()._scale;
        ]]></getter>
        <setter><![CDATA[
          if (val <= 0 || val == this.scale)
            return;

          let rootView = this.getRootView();
          rootView._setScale(val);
          this.finishFuzzyZoom();

          return val;
        ]]></setter>
      </property>

      <method name="_getView">
        <parameter name="contentView"/>
        <body>
          <![CDATA[
            if (!contentView) return null;

            // See if we have cached it.
            let id = contentView.id;
            let jsContentView = this._contentViews[id];
            if (jsContentView) {
              // Content view may have changed if it became inactive for a
              // little while.
              jsContentView._contentView = contentView;
              return jsContentView;
            }

            // Not cached. Create it.
            jsContentView = Object.create(this._contentViewPrototype);
            jsContentView.init(contentView);
            return jsContentView;
          ]]>
        </body>
      </method>

      <!-- Get root content view. -->
      <method name="getRootView">
        <body>
          <![CDATA[
            let contentView = this._contentViewManager.rootContentView;
            return this._getView(contentView) || this._contentNoop;
          ]]>
        </body>
      </method>

      <!-- Get contentView for position (x, y) relative to the browser element -->
      <method name="getViewAt">
        <parameter name="x"/>
        <parameter name="y"/>
        <body>
          <![CDATA[
            let manager = this._contentViewManager;
            let contentView = manager.getContentViewsIn(x, y, 0, 0, 0, 0)[0] ||
                              manager.rootContentView;
            return this._getView(contentView);
          ]]>
        </body>
      </method>

      <!-- Synchronize the CSS viewport with the projection viewport. -->
      <method name="_updateCSSViewport">
        <body>
          <![CDATA[
            let rootView = this._contentViewManager.rootContentView;
            this.messageManager.sendAsyncMessage("Content:ScrollTo", {
              x: rootView.scrollX / this.scale,
              y: rootView.scrollY / this.scale
            });
          ]]>
        </body>
      </method>

      <property name="active" onget="return this._active;">
        <setter><![CDATA[
            this._active = val;
            this.messageManager.sendAsyncMessage((val ? "Content:Activate" : "Content:Deactivate"), {});
            if (val)
              this.getRootView()._updateCacheViewport();
         ]]></setter>
      </property>

    </implementation>

  </binding>

</bindings>