chat/content/convbrowser.xml
author Gervase Markham <gerv@gerv.net>
Wed, 30 May 2012 17:48:24 +0100
changeset 12286 84ac3c71109811da751f0ef2d72108075938f094
parent 12280 9849318c2099d533f016f0e3d6194abd7eb41b4e
child 12288 9ed38b1c4bf19f15c996f71fb56f10e5441adc97
permissions -rw-r--r--
Bug 757018 - upgrade license to MPL 2.

<?xml version="1.0"?>

<!-- This Source Code Form is subject to the terms of the Mozilla Public
   - License, v. 2.0. If a copy of the MPL was not distributed with this
   - file, You can obtain one at http://mozilla.org/MPL/2.0/. -->

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

  <binding id="browser">
    <implementation type="application/javascript"
                    implements="nsIAccessibleProvider, nsIDOMEventListener,
                                nsIWebProgressListener, nsIController,
                                nsISelectionListener, nsIObserver">
      <property name="accessibleType" readonly="true">
        <getter>
          <![CDATA[
            return Components.interfaces.nsIAccessibleProvider.OuterDoc;
          ]]>
        </getter>
      </property>

      <property name="autoscrollEnabled">
        <getter>
          <![CDATA[
            if (this.getAttribute("autoscroll") == "false")
              return false;

            var enabled = true;
            try {
              enabled = Services.prefs.getBoolPref("general.autoScroll");
            }
            catch(ex) {
            }

            return enabled;
          ]]>
        </getter>
      </property>

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

      <property name="theme"
                readonly="true"
                onget="return this._theme || (this._theme = getCurrentTheme());"/>

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

      <field name="_loadState">0</field>

      <method name="init">
        <parameter name="aConversation"/>
        <body>
          <![CDATA[
            this._conv = aConversation;

            // init is called when the message style preview is
            // reloaded so we need to reset _theme.
            this._theme = null;

            // Prevent ongoing asynchronous message display from continuing.
            this._messageDisplayPending = false;

            // _loadState is 0 while loading conv.html and 1 while
            // loading the real conversation HTML.
            this._loadState = 0;

            this.docShell.charset = "UTF-8";
            const URI = "chrome://chat/content/conv.html";
            const flag = Ci.nsIWebNavigation.LOAD_FLAGS_NONE;
            this.webNavigation.loadURI(URI, flag, null, null, null);
            this.addProgressListener(this);

            if (this._scrollingView)
              this._autoScrollPopup.hidePopup();
          ]]>
        </body>
      </method>

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

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

      <property name="docShell"
                onget="return this._docShell || (this._docShell = this.boxObject.QueryInterface(Components.interfaces.nsIContainerBoxObject).docShell);"
                readonly="true"/>

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

      <property name="webNavigation"
                onget="return this._webNavigation || (this._webNavigation = this.docShell.QueryInterface(Components.interfaces.nsIWebNavigation));"
                readonly="true"/>


      <field name="_fastFind">null</field>
      <property name="fastFind"
                readonly="true">
        <getter>
        <![CDATA[
          if (!this._fastFind) {
            if (!("@mozilla.org/typeaheadfind;1" in Components.classes))
              return null;

            if (!this.docShell)
              return null;

            this._fastFind = Components.classes["@mozilla.org/typeaheadfind;1"]
                                       .createInstance(Components.interfaces.nsITypeAheadFind);
            this._fastFind.init(this.docShell);
          }
          return this._fastFind;
        ]]>
        </getter>
      </property>

      <property name="webProgress"
                readonly="true"
                onget="return this.docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIWebProgress);"/>

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

      <property name="contentWindow"
                readonly="true"
                onget="return this._contentWindow || (this._contentWindow = this.docShell.QueryInterface(Components.interfaces.nsIInterfaceRequestor).getInterface(Components.interfaces.nsIDOMWindow));"/>

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

      <property name="markupDocumentViewer"
                onget="return this.docShell.contentViewer.QueryInterface(Components.interfaces.nsIMarkupDocumentViewer);"
                readonly="true"/>

      <field name="magicCopyPref" readonly="true">"messenger.conversations.selections.magicCopyEnabled"</field>

      <property name="magicCopyEnabled"
                onget="return Services.prefs.getBoolPref(this.magicCopyPref);"
                readonly="true"/>

      <method name="addProgressListener">
        <parameter name="aListener"/>
        <body>
          <![CDATA[
            this.webProgress.addProgressListener(aListener, Components.interfaces.nsIWebProgress.NOTIFY_ALL);
          ]]>
        </body>
      </method>

      <method name="removeProgressListener">
        <parameter name="aListener"/>
        <body>
          <![CDATA[
            this.webProgress.removeProgressListener(aListener);
         ]]>
        </body>
      </method>

      <field name="mDestroyed">false</field>

      <constructor>
        <![CDATA[
          this.addEventListener("scroll", this.browserScroll);
          this.addEventListener("resize", this.browserResize);
          Services.prefs.addObserver(this.magicCopyPref, this, false);

          if (!("cleanupImMarkup" in window))
            Components.utils.import("resource:///modules/imContentSink.jsm");
          if (!("smileImMarkup" in window))
            Components.utils.import("resource:///modules/imSmileys.jsm");
          if (!("getCurrentTheme" in window))
            Components.utils.import("resource:///modules/imThemes.jsm");
        ]]>
      </constructor>

      <destructor>
        <![CDATA[
          this.destroy();
        ]]>
      </destructor>

      <!-- This is necessary because the destructor doesn't always get called when
           we are removed from a tabbrowser. This will be explicitly called by tabbrowser -->
      <method name="destroy">
        <body>
          <![CDATA[
            if (this.mDestroyed)
              return;
            this.mDestroyed = true;
            this._messageDisplayPending = false;

            if (this.mDragDropHandler)
              this.mDragDropHandler.detach();
            this.mDragDropHandler = null;

            this._fastFind = null;

            if (this._autoScrollNeedsCleanup) {
              // we polluted the global scope, so clean it up
              this._autoScrollPopup.parentNode.removeChild(this._autoScrollPopup);
            }

            Services.prefs.removeObserver(this.magicCopyPref, this);
            if (this.magicCopyEnabled)
              this.contentWindow.controllers.removeController(this);
          ]]>
        </body>
      </method>

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

      <method name="_updateAutoScrollEnabled">
        <body>
          <![CDATA[
            // Enable auto-scroll if the scrollbar is at the bottom.
            let body = this.contentDocument.getElementsByTagName("body")[0];
            this._autoScrollEnabled =
              body.scrollHeight <= body.scrollTop + body.clientHeight + 10;
            return this._autoScrollEnabled;
          ]]>
         </body>
      </method>

      <method name="_scrollToElement">
        <parameter name="aElt"/>
        <body>
          <![CDATA[
            aElt.scrollIntoView(true);
            this._scrollingIntoView = true;
          ]]>
         </body>
      </method>

      <field name="_textModifiers">[smileTextNode]</field>

      <method name="addTextModifier">
        <parameter name="aModifier"/>
        <body>
          <![CDATA[
            if (this._textModifiers.indexOf(aModifier) == -1)
              this._textModifiers.push(aModifier);
          ]]>
        </body>
      </method>

      <!-- These variables are reset in onStateChange. -->
      <field name="_lastMessage">null</field>
      <field name="_lastMessageIsContext">true</field>
      <field name="_firstNonContextElt">null</field>

      <field name="_pendingMessages">[]</field>
      <field name="_messageDisplayPending">false</field>
      <method name="appendMessage">
        <parameter name="aMsg"/>
        <parameter name="aContext"/>
        <parameter name="aFirstUnread"/>
        <body>
          <![CDATA[
            this._pendingMessages.push({msg: aMsg, context: aContext,
                                        firstUnread: aFirstUnread});
            if (this._messageDisplayPending)
              return;
            this._messageDisplayPending = true;
            Services.tm.mainThread.dispatch(this.displayPendingMessages.bind(this),
                                            Ci.nsIEventTarget.DISPATCH_NORMAL);
          ]]>
        </body>
      </method>

      <field name="progressBar">null</field>
      <!-- These variables are reset in onStateChange. -->
      <field name="_nextPendingMessageIndex">0</field>
      <field name="_displayPendingMessagesCalls">0</field>
      <method name="displayPendingMessages">
        <body>
          <![CDATA[
            if (!this._messageDisplayPending)
              return;

            let max = this._pendingMessages.length;
            let begin = Date.now();
            let i;
            for (i = this._nextPendingMessageIndex; i < max; ++i) {
              let msg = this._pendingMessages[i];
              this.displayMessage(msg.msg, msg.context, i + 1 < max,
                                  msg.firstUnread);
              if (Date.now() - begin > 40)
                break;
            }
            if (i < max - 1) {
              this._nextPendingMessageIndex = i + 1;
              if (this.progressBar) {
                // Show progress bar if after the third call (ca. 120ms)
                // less than half the messages have been displayed.
                if (++this._displayPendingMessagesCalls > 2 &&
                    max > 2 * this._nextPendingMessageIndex)
                  this.progressBar.hidden = false;
                this.progressBar.max = max;
                this.progressBar.value = this._nextPendingMessageIndex;
              }
              let event = document.createEvent("UIEvents");
              event.initUIEvent("MessagesDisplayed", false, false, window, 0);
              this.dispatchEvent(event);
              Services.tm.mainThread.dispatch(this.displayPendingMessages.bind(this),
                                              Ci.nsIEventTarget.DISPATCH_NORMAL);
              return;
            }
            this._messageDisplayPending = false;
            this._pendingMessages = [];
            this._nextPendingMessageIndex = 0;
            this._displayPendingMessagesCalls = 0;
            if (this.progressBar)
              this.progressBar.hidden = true;
          ]]>
        </body>
      </method>

      <method name="displayMessage">
        <parameter name="aMsg"/>
        <parameter name="aContext"/>
        <parameter name="aNoAutoScroll"/>
        <parameter name="aFirstUnread"/>
        <body>
          <![CDATA[
            let doc = this.contentDocument;
            let cs = Components.classes["@mozilla.org/txttohtmlconv;1"]
                               .getService(Ci.mozITXTToHTMLConv);
            /*
             * kStructPhrase creates tags for plaintext-markup like *bold*,
             * /italics/, etc. We always use this; the content filter will
             * filter it out if the user does not want styling.
             */
            let csFlags = cs.kStructPhrase;
            // Automatically find and link freetext URLs
            if (!aMsg.noLinkification)
              csFlags |= cs.kURLs;

            if (aFirstUnread)
              this.setUnreadRuler();

            let msg = aMsg.originalMessage;

            // The slash of a leading '/me' should not be used to
            // format as italic, so we remove the '/me' text before
            // scanning the HTML, and we add it back later.
            let meRegExp = /^((<[^>]+>)*)\/me /;
            let me = false;
            if (meRegExp.test(msg)) {
              me = true;
              msg = msg.replace(meRegExp, "$1");
            }

            msg = cs.scanHTML(msg.replace(/&/g, "&amp;"), csFlags)
                    .replace(/&amp;/g, "&");

            if (me)
              msg = msg.replace(/^((<[^>]+>)*)/, "$1/me ");

            aMsg.message = cleanupImMarkup(doc, msg.replace(/\r?\n/g, "<br/>"),
                                           null, this._textModifiers);

            let next = (aContext == this._lastMessageIsContext || aMsg.system) &&
                       isNextMessage(this.theme, aMsg, this._lastMessage);
            let newElt;
            if (next && aFirstUnread) {
              // If there wasn't an unread ruler, this would be a Next message.
              // Therefore, save that version for later.
              let html = getHTMLForMessage(aMsg, this.theme, next, aContext);
              let ruler = doc.getElementById("unread-ruler");
              ruler.nextMsgHtml = html;

              // Remember where the Next message(s) would have gone.
              let insert = doc.getElementById("insert");
              if (!insert) {
                insert = doc.createElement("div");
                ruler.parentNode.insertBefore(insert, ruler);
              }
              insert.id = "insert-before";

              next = false;
              html = getHTMLForMessage(aMsg, this.theme, next, aContext);
              newElt = insertHTMLForMessage(aMsg, html, doc, next);
              let marker = doc.createElement("div");
              marker.id = "end-of-split-block";
              newElt.parentNode.appendChild(marker);

              // Bracket the place where additional Next messages will be added,
              // if that's not after the end-of-split-block element.
              insert = doc.getElementById("insert");
              if (insert) {
                marker = doc.createElement("div");
                marker.id = "next-messages-start";
                insert.parentNode.insertBefore(marker, insert);
                marker = doc.createElement("div");
                marker.id = "next-messages-end";
                insert.parentNode.insertBefore(marker, insert.nextSibling);
              }
            }
            else {
              let html = getHTMLForMessage(aMsg, this.theme, next, aContext);
              newElt = insertHTMLForMessage(aMsg, html, doc, next);
            }

            if (!aNoAutoScroll) {
              newElt.getBoundingClientRect(); // avoid ireflow bugs
              if (this._autoScrollEnabled || this._updateAutoScrollEnabled())
                this._scrollToElement(newElt);
            }
            this._lastElement = newElt;
            this._lastMessage = aMsg;
            if (!aContext && !this._firstNonContextElt && !aMsg.system)
              this._firstNonContextElt = newElt;
            this._lastMessageIsContext = aContext;
          ]]>
        </body>
      </method>

      <method name="setUnreadRuler">
        <body>
        <![CDATA[
          // Remove any existing ruler (occurs when the window has lost focus).
          this.removeUnreadRuler();

          let doc = this.contentDocument;
          let ruler = doc.createElement("hr");
          ruler.id = "unread-ruler";
          doc.getElementById("Chat").appendChild(ruler);
       ]]>
        </body>
      </method>

      <method name="removeUnreadRuler">
        <body>
        <![CDATA[
          let doc = this.contentDocument;
          let ruler = doc.getElementById("unread-ruler");
          if (!ruler)
            return;

          // If a message block was split by the ruler, rejoin it.
          let moveTo = doc.getElementById("insert-before");
          if (moveTo) {
            // Protect an existing insert node.
            let actualInsert = doc.getElementById("insert");
            if (actualInsert)
              actualInsert.id = "actual-insert";

            // Add first message following the ruler as a Next type message.
            let range = doc.createRange();
            let moveToParent = moveTo.parentNode;
            range.selectNode(moveToParent);
            let documentFragment = range.createContextualFragment(ruler.nextMsgHtml);
            moveToParent.insertBefore(documentFragment, moveTo);

            // If this added an insert node, insert the next messages there.
            let insert = doc.getElementById("insert");
            if (insert) {
              moveToParent.removeChild(moveTo);
              moveTo = insert;
              moveToParent = moveTo.parentNode;
            }

            // Move remaining messages from the message block following the ruler.
            let nextMessagesStart = doc.getElementById("next-messages-start");
            if (nextMessagesStart) {
              range = doc.createRange();
              range.setStartAfter(nextMessagesStart);
              range.setEndBefore(doc.getElementById("next-messages-end"));
              moveToParent.insertBefore(range.extractContents(), moveTo);
            }
            moveToParent.removeChild(moveTo);

            // Restore existing insert node.
            if (actualInsert)
              actualInsert.id = "insert";

            // Delete surplus message block.
            range = doc.createRange();
            range.setStartAfter(ruler);
            range.setEndAfter(doc.getElementById("end-of-split-block"));
            range.deleteContents();
          }
          ruler.parentNode.removeChild(ruler);
        ]]>
        </body>
      </method>

      <method name="_getSections">
        <body>
        <![CDATA[
          // If a section is displayed below this point, we assume not enough of
          // it is visible, so we must scroll to it.
          // The 3/4 constant is arbitrary, but it has to be greater than 1/2.
          this._maximalSectionOffset = Math.round(this.clientHeight * 3 / 4);

          // Get list of current section elements.
          let sectionElements = [];
          if (this._firstNonContextElt)
            sectionElements.push(this._firstNonContextElt);
          let ruler = this.contentDocument.getElementById("unread-ruler");
          if (ruler)
            sectionElements.push(ruler);

          // Return ordered array of sections with entries
          // [Y, scrollY such that Y is centered]
          let sections = [];
          let maxY = this.contentWindow.scrollMaxY;
          for (let i = 0; i < sectionElements.length; ++i) {
            let y = sectionElements[i].offsetTop;
            // The section is unnecessary if close to top/bottom of conversation.
            if (y < this._maximalSectionOffset || maxY < y)
              continue;
            sections.push([y, y - Math.round(this.clientHeight / 2)]);
          }
          sections.sort(function(a, b) a[0] - b[0]);
          return sections;
        ]]>
        </body>
      </method>

      <method name="scrollToPreviousSection">
        <body>
        <![CDATA[
          let sections = this._getSections();
          let y = this.contentWindow.scrollY;
          let newY = 0;
          for (let i = sections.length - 1; i >= 0; --i) {
            let section = sections[i];
            if (y > section[0]) {
              newY = section[1];
              break;
            }
          }
          this.contentWindow.scrollTo(0, newY);
          this._updateAutoScrollEnabled();
        ]]>
        </body>
      </method>

      <method name="scrollToNextSection">
        <body>
        <![CDATA[
          let sections = this._getSections();
          let y = this.contentWindow.scrollY;
          let newY = this.contentWindow.scrollMaxY;
          for (let i = 0; i < sections.length; ++i) {
            let section = sections[i];
            if (y + this._maximalSectionOffset < section[0]) {
              newY = section[1];
              break;
            }
          }
          this.contentWindow.scrollTo(0, newY);
          this._updateAutoScrollEnabled();
        ]]>
        </body>
      </method>

      <method name="browserScroll">
        <parameter name="event"/>
        <body>
          <![CDATA[
            if (this._scrollingIntoView) {
              // We have explicitely requested a scrollIntoView, ignore the event
              this._scrollingIntoView = false;
              this._lastScrollHeight = this.scrollHeight;
              this._lastScrollWidth = this.scrollWidth;
              return;
            }

            if (!("_lastScrollHeight" in this) ||
                this._lastScrollHeight != this.scrollHeight ||
                this._lastScrollWidth != this.scrollWidth) {
              // if the scrollheight changed, we are resizing the content area,
              // don't stop the auto scroll.
              this._lastScrollHeight = this.scrollHeight;
              this._lastScrollWidth = this.scrollWidth;
              return;
            }

            // Enable or disable auto-scroll based on the scrollbar position
            this._updateAutoScrollEnabled();
          ]]>
         </body>
      </method>

      <method name="browserResize">
        <parameter name="event"/>
        <body>
          <![CDATA[
            if (this._autoScrollEnabled && this._lastElement) {
              // The content area was resized and auto-scroll is enabled,
              // make sure the last inserted element is still visible
              this._scrollToElement(this._lastElement);
            }
          ]]>
        </body>
      </method>

      <!-- nsIObserver implementation -->
      <method name="observe">
        <parameter name="aSubject"/>
        <parameter name="aTopic"/>
        <parameter name="aData"/>
        <body>
        <![CDATA[
          if (aTopic != "nsPref:changed")
            throw "Bad notification";

          var clipboard =
            Components.classes["@mozilla.org/widget/clipboard;1"]
                      .getService(Components.interfaces.nsIClipboard);

          if (this.magicCopyEnabled) {
            this.contentWindow.controllers.insertControllerAt(0, this);
            if (clipboard.supportsSelectionClipboard()) {
              this.contentWindow.getSelection()
                  .QueryInterface(Components.interfaces.nsISelectionPrivate)
                  .addSelectionListener(this);
            }
          }
          else {
            this.contentWindow.controllers.removeController(this);
            if (clipboard.supportsSelectionClipboard()) {
               this.contentWindow.getSelection()
                   .QueryInterface(Components.interfaces.nsISelectionPrivate)
                   .removeSelectionListener(this);
            }
          }
        ]]>
        </body>
      </method>

      <!-- nsIController implementation -->
      <method name="supportsCommand">
        <parameter name="aCommand"/>
        <body>
          <![CDATA[
            return aCommand == "cmd_copy" || aCommand == "cmd_cut";
          ]]>
        </body>
      </method>

      <method name="isCommandEnabled">
        <parameter name="aCommand"/>
        <body>
          <![CDATA[
            return aCommand == "cmd_copy" &&
                   !this.contentWindow.getSelection().isCollapsed;
          ]]>
        </body>
      </method>

      <method name="doCommand">
        <parameter name="aCommand"/>
        <body>
          <![CDATA[
            let selection = this.contentWindow.getSelection();
            if (selection.isCollapsed)
              return;

            Components.classes["@mozilla.org/widget/clipboardhelper;1"]
                      .getService(Ci.nsIClipboardHelper)
                      .copyString(serializeSelection(selection));
          ]]>
        </body>
      </method>

      <method name="onEvent">
        <parameter name="aCommand"/>
      </method>

      <!-- nsISelectionListener implementation -->
      <method name="notifySelectionChanged">
        <parameter name="aDocument"/>
        <parameter name="aSelection"/>
        <parameter name="aReason"/>
        <body>
          <![CDATA[
            if (!(aReason & Ci.nsISelectionListener.MOUSEUP_REASON   ||
                  aReason & Ci.nsISelectionListener.SELECTALL_REASON ||
                  aReason & Ci.nsISelectionListener.KEYPRESS_REASON))
              return; // we are still dragging, don't bother with the selection

            Components.classes["@mozilla.org/widget/clipboardhelper;1"]
                      .getService(Ci.nsIClipboardHelper)
                      .copyStringToClipboard(serializeSelection(aSelection),
                                             Ci.nsIClipboard.kSelectionClipboard);
          ]]>
        </body>
      </method>

      <!-- nsIWebProgressListener implementation -->
      <method name="onStateChange">
        <parameter name="aProgress"/>
        <parameter name="aRequest"/>
        <parameter name="aStateFlags"/>
        <parameter name="aStatus"/>
        <body>
          <![CDATA[
            const WPL = Components.interfaces.nsIWebProgressListener;
            if ((aStateFlags & WPL.STATE_IS_DOCUMENT) &&
                (aStateFlags & WPL.STATE_STOP)) {
              if (!this._loadState) {
                try {
                  initHTMLDocument(this._conv, this.theme, this.contentDocument);
                } catch(e) {
                  Components.utils.reportError(e);
                }
                this._loadState = 1;
                return;
              }
              this.removeProgressListener(this);

              if (this.magicCopyEnabled) {
                this.contentWindow.controllers.insertControllerAt(0, this);

                var clipboard =
                  Components.classes["@mozilla.org/widget/clipboard;1"]
                            .getService(Components.interfaces.nsIClipboard);
                if (clipboard.supportsSelectionClipboard()) {
                  this.contentWindow.getSelection()
                      .QueryInterface(Components.interfaces.nsISelectionPrivate)
                      .addSelectionListener(this);
                }
              }

              // We need to reset these variables here to avoid a race
              // condition if we are starting to display a new conversation
              // but the display of the previous conversation wasn't finished.
              // This can happen if the user quickly changes the selected
              // conversation in the log viewer.
              this._lastMessage = null;
              this._lastMessageIsContext = true;
              this._firstNonContextElt = null;
              this._messageDisplayPending = false;
              this._pendingMessages = [];
              this._nextPendingMessageIndex = 0;
              this._displayPendingMessagesCalls = 0;

              Services.obs.notifyObservers(this, "conversation-loaded", null);
            }
          ]]>
        </body>
      </method>

      <method name="onProgressChange">
        <parameter name="aProgress"/>
        <parameter name="aRequest"/>
        <parameter name="aCurSelf"/>
        <parameter name="aMaxSelf"/>
        <parameter name="aCurTotal"/>
        <parameter name="aMaxTotal"/>
      </method>

      <method name="onLocationChange">
        <parameter name="aProgress"/>
        <parameter name="aRequest"/>
        <parameter name="aLocation"/>
      </method>

      <method name="onStatusChange">
        <parameter name="aProgress"/>
        <parameter name="aRequest"/>
        <parameter name="aStatus"/>
        <parameter name="aMessage"/>
      </method>

      <method name="onSecurityChange">
        <parameter name="aProgress"/>
        <parameter name="aRequest"/>
        <parameter name="aState"/>
      </method>


      <!-- autoscroll stuff -->
      <field name="_AUTOSCROLL_SPEED">3</field>
      <field name="_AUTOSCROLL_SNAP">10</field>
      <field name="_scrollingView">null</field>
      <field name="_autoScrollTimer">null</field>
      <field name="_startX">null</field>
      <field name="_startY">null</field>
      <field name="_screenX">null</field>
      <field name="_screenY">null</field>
      <field name="_autoScrollPopup">null</field>
      <field name="_autoScrollNeedsCleanup">false</field>

      <method name="stopScroll">
        <body>
          <![CDATA[
            if (this._scrollingView) {
              this._scrollingView = null;
              window.removeEventListener("mousemove", this, true);
              window.removeEventListener("mousedown", this, true);
              window.removeEventListener("mouseup", this, true);
              window.removeEventListener("contextmenu", this, true);
              clearInterval(this._autoScrollTimer);
            }
         ]]>
       </body>
      </method>

      <method name="_createAutoScrollPopup">
        <body>
          <![CDATA[
            const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
            var popup = document.createElementNS(XUL_NS, "popup");
            popup.className = "autoscroller";
            return popup;
          ]]>
        </body>
      </method>

      <method name="startScroll">
        <parameter name="event"/>
        <body>
          <![CDATA[
            if (!this._autoScrollPopup) {
              if (this.hasAttribute("autoscrollpopup")) {
                // our creator provided a popup to share
                this._autoScrollPopup = document.getElementById(this.getAttribute("autoscrollpopup"));
              }
              else {
                // we weren't provided a popup; we have to use the global scope
                this._autoScrollPopup = this._createAutoScrollPopup();
                document.documentElement.appendChild(this._autoScrollPopup);
                this._autoScrollNeedsCleanup = true;
              }
            }

            this._autoScrollPopup.addEventListener("popuphidden", this, true);

            // we need these attributes so themers don't need to create per-platform packages
            if (screen.colorDepth > 8) { // need high color for transparency
              // Exclude second-rate platforms
              this._autoScrollPopup.setAttribute("transparent", !/BeOS|OS\/2|Photon/.test(navigator.appVersion));
              // Enable translucency on Windows and Mac
              this._autoScrollPopup.setAttribute("translucent", /Win|Mac/.test(navigator.platform));
            }

            this._scrollingView = event.originalTarget.ownerDocument.defaultView;
            if (this._scrollingView.scrollMaxX > 0) {
              this._autoScrollPopup.setAttribute("scrolldir", this._scrollingView.scrollMaxY > 0 ? "NSEW" : "EW");
            }
            else if (this._scrollingView.scrollMaxY > 0) {
              this._autoScrollPopup.setAttribute("scrolldir", "NS");
            }
            else {
              this._scrollingView = null; // abort scrolling
              return;
            }

            document.popupNode = null;
            this._autoScrollPopup.showPopup(document.documentElement,
                                            event.screenX,
                                            event.screenY,
                                            "popup", null, null);
            this._ignoreMouseEvents = true;
            this._startX = event.screenX;
            this._startY = event.screenY;
            this._screenX = event.screenX;
            this._screenY = event.screenY;

            window.addEventListener("mousemove", this, true);
            window.addEventListener("mousedown", this, true);
            window.addEventListener("mouseup", this, true);
            window.addEventListener("contextmenu", this, true);

            this._scrollErrorX = 0;
            this._scrollErrorY = 0;

            this._autoScrollTimer = setInterval(function(self) { self.autoScrollLoop(); },
                                                20, this);
         ]]>
        </body>
      </method>

      <method name="_roundToZero">
        <parameter name="num"/>
        <body>
          <![CDATA[
            if (num > 0)
              return Math.floor(num);
            return Math.ceil(num);
          ]]>
        </body>
      </method>

      <method name="_accelerate">
        <parameter name="curr"/>
        <parameter name="start"/>
        <body>
          <![CDATA[
            const kSpeed = 12;
            var val = (curr - start) / kSpeed;

            if (val > 1)
              return val * Math.sqrt(val) - 1;
            if (val < -1)
              return val * Math.sqrt(-val) + 1;
            return 0;
          ]]>
        </body>
      </method>

      <method name="autoScrollLoop">
        <body>
          <![CDATA[
            var x = this._accelerate(this._screenX, this._startX);
            var y = this._accelerate(this._screenY, this._startY);

            var desiredScrollX = this._scrollErrorX + x;
            var actualScrollX = this._roundToZero(desiredScrollX);
            this._scrollErrorX = (desiredScrollX - actualScrollX);
            var desiredScrollY = this._scrollErrorY + y;
            var actualScrollY = this._roundToZero(desiredScrollY);
            this._scrollErrorY = (desiredScrollY - actualScrollY);

            this._scrollingView.scrollBy(actualScrollX, actualScrollY);
          ]]>
        </body>
      </method>

      <method name="isAutoscrollBlocker">
        <parameter name="node"/>
        <body>
          <![CDATA[
            var mmPaste = false;
            var mmScrollbarPosition = false;

            try {
              mmPaste = Services.prefs.getBoolPref("middlemouse.paste");
            }
            catch (ex) {
            }

            try {
              mmScrollbarPosition = Services.prefs.getBoolPref("middlemouse.scrollbarPosition");
            }
            catch (ex) {
            }

            while (node) {
              if ((node instanceof HTMLAnchorElement || node instanceof HTMLAreaElement) && node.hasAttribute("href"))
                return true;

              if (mmPaste && (node instanceof HTMLInputElement || node instanceof HTMLTextAreaElement))
                return true;

              if (node instanceof XULElement && mmScrollbarPosition
                  && (node.localName == "scrollbar" || node.localName == "scrollcorner"))
                return true;

              node = node.parentNode;
            }
            return false;
          ]]>
        </body>
      </method>

      <method name="handleEvent">
        <parameter name="aEvent"/>
        <body>
          <![CDATA[
            if (this._scrollingView) {
              switch(aEvent.type) {
                case "mousemove": {
                  this._screenX = aEvent.screenX;
                  this._screenY = aEvent.screenY;

                  var x = this._screenX - this._startX;
                  var y = this._screenY - this._startY;

                  if ((x > this._AUTOSCROLL_SNAP || x < -this._AUTOSCROLL_SNAP) ||
                      (y > this._AUTOSCROLL_SNAP || y < -this._AUTOSCROLL_SNAP))
                    this._ignoreMouseEvents = false;
                  break;
                }
                case "mouseup":
                case "mousedown":
                case "contextmenu": {
                  if (!this._ignoreMouseEvents)
                    this._autoScrollPopup.hidePopup();
                  this._ignoreMouseEvents = false;
                  break;
                }
                case "popuphidden": {
                  this._autoScrollPopup.removeEventListener("popuphidden", this, true);
                  this.stopScroll();
                  break;
                }
              }
            }
          ]]>
        </body>
      </method>

      <method name="swapDocShells">
        <parameter name="aOtherBrowser"/>
        <body>
        <![CDATA[
          aOtherBrowser.destroy();

          var magicCopyEnabled = this.magicCopyEnabled;

          if (magicCopyEnabled) {
            // We need to remove the selection listener (unix only)
            // before _contentWindow is changed.
            var clipboard = Components.classes["@mozilla.org/widget/clipboard;1"]
                                      .getService(Ci.nsIClipboard);
            if (clipboard.supportsSelectionClipboard()) {
              aOtherBrowser.contentWindow.getSelection()
                           .QueryInterface(Components.interfaces.nsISelectionPrivate)
                           .removeSelectionListener(aOtherBrowser);
            }
          }

          // and the progress listener too!
          this.removeProgressListener(this);

          // We need to swap fields that are tied to our docshell or related to
          // the loaded page
          // Fields which are built as a result of notifactions (pageshow/hide,
          // DOMLinkAdded/Removed, onStateChange) should not be swapped here,
          // because these notifications are dispatched again once the docshells
          // are swapped.
          var fieldsToSwap = ["_docShell", "_fastFind", "_contentWindow", "_webNavigation",
                              "_theme", "_textModifiers", "_autoScrollEnabled",
                              "_lastElement", "_lastMessage", "_lastMessageIsContext",
                              "_firstNonContextElt"];

          var ourFieldValues = {};
          var otherFieldValues = {};
          for each (var field in fieldsToSwap) {
            ourFieldValues[field] = this[field];
            otherFieldValues[field] = aOtherBrowser[field];
          }
          this.QueryInterface(Components.interfaces.nsIFrameLoaderOwner)
              .swapFrameLoaders(aOtherBrowser);
          for each (var field in fieldsToSwap) {
            this[field] = otherFieldValues[field];
            aOtherBrowser[field] = ourFieldValues[field];
          }

          if (!magicCopyEnabled)
            return;

          // Now that we have the new _contentWindow, we can add back our controller
          this.contentWindow.controllers.insertControllerAt(0, this);

          // and our selection listener!
          if (clipboard.supportsSelectionClipboard()) {
            this.contentWindow.getSelection()
                .QueryInterface(Components.interfaces.nsISelectionPrivate)
                .addSelectionListener(this);
          }
        ]]>
        </body>
      </method>
    </implementation>

    <handlers>
      <handler event="click">
        <![CDATA[
          // Right click should open the context menu.
          if (event.button == 2)
            return;

          // The 'click' event is fired even when the link is
          // activated with the keyboard.

          // The event target may be a descendant of the actual link.
          let url;
          for (let elem = event.target; elem; elem = elem.parentNode) {
            if (elem instanceof HTMLAnchorElement) {
              url = elem.href;
              if (url)
                break;
            }
          }
          if (url) {
            let uri = Services.io.newURI(url, null, null);

            // http and https are the only schemes that are both
            // allowed by our IM filters and exposed.
            if (!uri.schemeIs("http") && !uri.schemeIs("https"))
              return;

            event.preventDefault();
            event.stopPropagation();

            // loadUrl can throw if the default browser is misconfigured.
            Components.classes["@mozilla.org/uriloader/external-protocol-service;1"]
                      .getService(Components.interfaces.nsIExternalProtocolService)
                      .loadUrl(uri);
          }
        ]]>
      </handler>

      <handler event="keypress" modifiers="shift" keycode="VK_PAGE_UP"
               action="this.docShell.QueryInterface(Components.interfaces.nsITextScroll).scrollByPages(-1);"/>

      <handler event="keypress" modifiers="shift" keycode="VK_PAGE_DOWN"
               action="this.docShell.QueryInterface(Components.interfaces.nsITextScroll).scrollByPages(1);"/>

      <handler event="keypress" modifiers="alt" keycode="VK_PAGE_UP"
               action="this.scrollToPreviousSection();"/>

      <handler event="keypress" modifiers="alt" keycode="VK_PAGE_DOWN"
               action="this.scrollToNextSection();"/>

      <handler event="keypress" keycode="VK_HOME"
               action="this.scrollToPreviousSection(); event.preventDefault();"/>

      <handler event="keypress" keycode="VK_END"
               action="this.scrollToNextSection(); event.preventDefault();"/>

      <handler event="keypress" keycode="VK_F7" group="system">
        <![CDATA[
          if (event.getPreventDefault() || !event.isTrusted)
            return;

          var isEnabled = Services.prefs.getBoolPref("accessibility.browsewithcaret_shortcut.enabled");
          if (!isEnabled)
            return;

          // Toggle browse with caret mode
          try {
            var browseWithCaretOn = Services.prefs.getBoolPref("accessibility.browsewithcaret");
            Services.prefs.setBoolPref("accessibility.browsewithcaret", !browseWithCaretOn);
          } catch (ex) {
          }
        ]]>
      </handler>

      <handler event="mousedown" phase="capturing">
        <![CDATA[
          if (!this._scrollingView && event.button == 1) {
            if (!this.autoscrollEnabled  ||
                this.isAutoscrollBlocker(event.originalTarget))
              return;

            this.startScroll(event);
          }
        ]]>
      </handler>
    </handlers>
  </binding>
</bindings>