browser/base/content/socialchat.xml

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/browser/base/content/socialchat.xml	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,763 @@
     1.4 +<?xml version="1.0"?>
     1.5 +
     1.6 +<bindings id="socialChatBindings"
     1.7 +    xmlns="http://www.mozilla.org/xbl"
     1.8 +    xmlns:xbl="http://www.mozilla.org/xbl"
     1.9 +    xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
    1.10 +
    1.11 +  <binding id="chatbox">
    1.12 +    <content orient="vertical" mousethrough="never">
    1.13 +      <xul:hbox class="chat-titlebar" xbl:inherits="minimized,selected,activity" align="baseline">
    1.14 +        <xul:hbox flex="1" onclick="document.getBindingParent(this).onTitlebarClick(event);">
    1.15 +          <xul:image class="chat-status-icon" xbl:inherits="src=image"/>
    1.16 +          <xul:label class="chat-title" flex="1" xbl:inherits="value=label" crop="center"/>
    1.17 +        </xul:hbox>
    1.18 +        <xul:toolbarbutton anonid="notification-icon" class="notification-anchor-icon chat-toolbarbutton"
    1.19 +                           oncommand="document.getBindingParent(this).showNotifications(); event.stopPropagation();"/>
    1.20 +        <xul:toolbarbutton anonid="minimize" class="chat-minimize-button chat-toolbarbutton"
    1.21 +                           oncommand="document.getBindingParent(this).toggle();"/>
    1.22 +        <xul:toolbarbutton anonid="swap" class="chat-swap-button chat-toolbarbutton"
    1.23 +                           oncommand="document.getBindingParent(this).swapWindows();"/>
    1.24 +        <xul:toolbarbutton anonid="close" class="chat-close-button chat-toolbarbutton"
    1.25 +                           oncommand="document.getBindingParent(this).close();"/>
    1.26 +      </xul:hbox>
    1.27 +      <xul:browser anonid="content" class="chat-frame" flex="1"
    1.28 +                  context="contentAreaContextMenu"
    1.29 +                  disableglobalhistory="true"
    1.30 +                  tooltip="aHTMLTooltip"
    1.31 +                  xbl:inherits="src,origin" type="content"/>
    1.32 +    </content>
    1.33 +
    1.34 +    <implementation implements="nsIDOMEventListener">
    1.35 +      <constructor><![CDATA[
    1.36 +        let Social = Components.utils.import("resource:///modules/Social.jsm", {}).Social;
    1.37 +        this.content.__defineGetter__("popupnotificationanchor",
    1.38 +                                      () => document.getAnonymousElementByAttribute(this, "anonid", "notification-icon"));
    1.39 +        Social.setErrorListener(this.content, function(aBrowser) {
    1.40 +          aBrowser.webNavigation.loadURI("about:socialerror?mode=compactInfo&origin=" +
    1.41 +                                 encodeURIComponent(aBrowser.getAttribute("origin")),
    1.42 +                                 null, null, null, null);
    1.43 +        });
    1.44 +        if (!this.chatbar) {
    1.45 +          document.getAnonymousElementByAttribute(this, "anonid", "minimize").hidden = true;
    1.46 +          document.getAnonymousElementByAttribute(this, "anonid", "close").hidden = true;
    1.47 +        }
    1.48 +        let contentWindow = this.contentWindow;
    1.49 +        this.addEventListener("DOMContentLoaded", function DOMContentLoaded(event) {
    1.50 +          if (event.target != this.contentDocument)
    1.51 +            return;
    1.52 +          this.removeEventListener("DOMContentLoaded", DOMContentLoaded, true);
    1.53 +          this.isActive = !this.minimized;
    1.54 +          // process this._callbacks, then set to null so the chatbox creator
    1.55 +          // knows to make new callbacks immediately.
    1.56 +          if (this._callbacks) {
    1.57 +            for (let callback of this._callbacks) {
    1.58 +              if (callback)
    1.59 +                callback(contentWindow);
    1.60 +            }
    1.61 +            this._callbacks = null;
    1.62 +          }
    1.63 +
    1.64 +          // content can send a socialChatActivity event to have the UI update.
    1.65 +          let chatActivity = function() {
    1.66 +            this.setAttribute("activity", true);
    1.67 +            if (this.chatbar)
    1.68 +              this.chatbar.updateTitlebar(this);
    1.69 +          }.bind(this);
    1.70 +          contentWindow.addEventListener("socialChatActivity", chatActivity);
    1.71 +          contentWindow.addEventListener("unload", function unload() {
    1.72 +            contentWindow.removeEventListener("unload", unload);
    1.73 +            contentWindow.removeEventListener("socialChatActivity", chatActivity);
    1.74 +          });
    1.75 +        }, true);
    1.76 +        if (this.src)
    1.77 +          this.setAttribute("src", this.src);
    1.78 +      ]]></constructor>
    1.79 +
    1.80 +      <field name="content" readonly="true">
    1.81 +        document.getAnonymousElementByAttribute(this, "anonid", "content");
    1.82 +      </field>
    1.83 +
    1.84 +      <property name="contentWindow">
    1.85 +        <getter>
    1.86 +          return this.content.contentWindow;
    1.87 +        </getter>
    1.88 +      </property>
    1.89 +
    1.90 +      <property name="contentDocument">
    1.91 +        <getter>
    1.92 +          return this.content.contentDocument;
    1.93 +        </getter>
    1.94 +      </property>
    1.95 +
    1.96 +      <property name="minimized">
    1.97 +        <getter>
    1.98 +          return this.getAttribute("minimized") == "true";
    1.99 +        </getter>
   1.100 +        <setter><![CDATA[
   1.101 +          // Note that this.isActive is set via our transitionend handler so
   1.102 +          // the content doesn't see intermediate values.
   1.103 +          let parent = this.chatbar;
   1.104 +          if (val) {
   1.105 +            this.setAttribute("minimized", "true");
   1.106 +            // If this chat is the selected one a new one needs to be selected.
   1.107 +            if (parent && parent.selectedChat == this)
   1.108 +              parent._selectAnotherChat();
   1.109 +          } else {
   1.110 +            this.removeAttribute("minimized");
   1.111 +            // this chat gets selected.
   1.112 +            if (parent)
   1.113 +              parent.selectedChat = this;
   1.114 +          }
   1.115 +        ]]></setter>
   1.116 +      </property>
   1.117 +
   1.118 +      <property name="chatbar">
   1.119 +        <getter>
   1.120 +          if (this.parentNode.nodeName == "chatbar")
   1.121 +            return this.parentNode;
   1.122 +          return null;
   1.123 +        </getter>
   1.124 +      </property>
   1.125 +
   1.126 +      <property name="isActive">
   1.127 +        <getter>
   1.128 +          return this.content.docShell.isActive;
   1.129 +        </getter>
   1.130 +        <setter>
   1.131 +          this.content.docShell.isActive = !!val;
   1.132 +
   1.133 +          // let the chat frame know if it is being shown or hidden
   1.134 +          let evt = this.contentDocument.createEvent("CustomEvent");
   1.135 +          evt.initCustomEvent(val ? "socialFrameShow" : "socialFrameHide", true, true, {});
   1.136 +          this.contentDocument.documentElement.dispatchEvent(evt);
   1.137 +        </setter>
   1.138 +      </property>
   1.139 +      
   1.140 +      <method name="showNotifications">
   1.141 +        <body><![CDATA[
   1.142 +        PopupNotifications._reshowNotifications(this.content.popupnotificationanchor,
   1.143 +                                                this.content);
   1.144 +        ]]></body>
   1.145 +      </method>
   1.146 +
   1.147 +      <method name="swapDocShells">
   1.148 +        <parameter name="aTarget"/>
   1.149 +        <body><![CDATA[
   1.150 +          aTarget.setAttribute('label', this.contentDocument.title);
   1.151 +          aTarget.src = this.src;
   1.152 +          aTarget.content.setAttribute("origin", this.content.getAttribute("origin"));
   1.153 +          aTarget.content.popupnotificationanchor.className = this.content.popupnotificationanchor.className;
   1.154 +          this.content.socialErrorListener.remove();
   1.155 +          aTarget.content.socialErrorListener.remove();
   1.156 +          this.content.swapDocShells(aTarget.content);
   1.157 +          Social.setErrorListener(this.content, function(aBrowser) {}); // 'this' will be destroyed soon.
   1.158 +          Social.setErrorListener(aTarget.content, function(aBrowser) {
   1.159 +            aBrowser.webNavigation.loadURI("about:socialerror?mode=compactInfo&origin=" +
   1.160 +                                 encodeURIComponent(aBrowser.getAttribute("origin")),
   1.161 +                                 null, null, null, null);
   1.162 +          });
   1.163 +        ]]></body>
   1.164 +      </method>
   1.165 +
   1.166 +      <method name="onTitlebarClick">
   1.167 +        <parameter name="aEvent"/>
   1.168 +        <body><![CDATA[
   1.169 +          if (!this.chatbar)
   1.170 +            return;
   1.171 +          if (aEvent.button == 0) { // left-click: toggle minimized.
   1.172 +            this.toggle();
   1.173 +            // if we restored it, we want to focus it.
   1.174 +            if (!this.minimized)
   1.175 +              this.chatbar.focus();
   1.176 +          } else if (aEvent.button == 1) // middle-click: close chat
   1.177 +            this.close();
   1.178 +        ]]></body>
   1.179 +      </method>
   1.180 +
   1.181 +      <method name="close">
   1.182 +        <body><![CDATA[
   1.183 +        if (this.chatbar)
   1.184 +          this.chatbar.remove(this);
   1.185 +        else
   1.186 +          window.close();
   1.187 +        ]]></body>
   1.188 +      </method>
   1.189 +
   1.190 +      <method name="swapWindows">
   1.191 +        <body><![CDATA[
   1.192 +        let provider = Social._getProviderFromOrigin(this.content.getAttribute("origin"));
   1.193 +        if (this.chatbar) {
   1.194 +          this.chatbar.detachChatbox(this, { "centerscreen": "yes" }, win => {
   1.195 +            win.document.title = provider.name;
   1.196 +          });
   1.197 +        } else {
   1.198 +          // attach this chatbox to the topmost browser window
   1.199 +          let findChromeWindowForChats = Cu.import("resource://gre/modules/MozSocialAPI.jsm").findChromeWindowForChats;
   1.200 +          let win = findChromeWindowForChats();
   1.201 +          let chatbar = win.SocialChatBar.chatbar;
   1.202 +          chatbar.openChat(provider, "about:blank", win => {
   1.203 +            let cb = chatbar.selectedChat;
   1.204 +            this.swapDocShells(cb);
   1.205 +
   1.206 +            // chatboxForURL is a map of URL -> chatbox used to avoid opening
   1.207 +            // duplicate chat windows. Ensure reattached chat windows aren't
   1.208 +            // registered with about:blank as their URL, otherwise reattaching
   1.209 +            // more than one chat window isn't possible.
   1.210 +            chatbar.chatboxForURL.delete("about:blank");
   1.211 +            chatbar.chatboxForURL.set(this.src, Cu.getWeakReference(cb));
   1.212 +
   1.213 +            chatbar.focus();
   1.214 +            this.close();
   1.215 +          });
   1.216 +        }
   1.217 +        ]]></body>
   1.218 +      </method>
   1.219 +
   1.220 +      <method name="toggle">
   1.221 +        <body><![CDATA[
   1.222 +          this.minimized = !this.minimized;
   1.223 +        ]]></body>
   1.224 +      </method>
   1.225 +    </implementation>
   1.226 +
   1.227 +    <handlers>
   1.228 +      <handler event="focus" phase="capturing">
   1.229 +        if (this.chatbar)
   1.230 +          this.chatbar.selectedChat = this;
   1.231 +      </handler>
   1.232 +      <handler event="DOMTitleChanged"><![CDATA[
   1.233 +        this.setAttribute('label', this.contentDocument.title);
   1.234 +        if (this.chatbar)
   1.235 +          this.chatbar.updateTitlebar(this);
   1.236 +      ]]></handler>
   1.237 +      <handler event="DOMLinkAdded"><![CDATA[
   1.238 +        // much of this logic is from DOMLinkHandler in browser.js
   1.239 +        // this sets the presence icon for a chat user, we simply use favicon style updating
   1.240 +        let link = event.originalTarget;
   1.241 +        let rel = link.rel && link.rel.toLowerCase();
   1.242 +        if (!link || !link.ownerDocument || !rel || !link.href)
   1.243 +          return;
   1.244 +        if (link.rel.indexOf("icon") < 0)
   1.245 +          return;
   1.246 +
   1.247 +        let ContentLinkHandler = Cu.import("resource:///modules/ContentLinkHandler.jsm", {}).ContentLinkHandler;
   1.248 +        let uri = ContentLinkHandler.getLinkIconURI(link);
   1.249 +        if (!uri)
   1.250 +          return;
   1.251 +
   1.252 +        // we made it this far, use it
   1.253 +        this.setAttribute('image', uri.spec);
   1.254 +        if (this.chatbar)
   1.255 +          this.chatbar.updateTitlebar(this);
   1.256 +      ]]></handler>
   1.257 +      <handler event="transitionend">
   1.258 +        if (this.isActive == this.minimized)
   1.259 +          this.isActive = !this.minimized;
   1.260 +      </handler>
   1.261 +    </handlers>
   1.262 +  </binding>
   1.263 +
   1.264 +  <binding id="chatbar">
   1.265 +    <content>
   1.266 +      <xul:hbox align="end" pack="end" anonid="innerbox" class="chatbar-innerbox" mousethrough="always" flex="1">
   1.267 +        <xul:spacer flex="1" anonid="spacer" class="chatbar-overflow-spacer"/>
   1.268 +        <xul:toolbarbutton anonid="nub" class="chatbar-button" type="menu" collapsed="true" mousethrough="never">
   1.269 +          <xul:menupopup anonid="nubMenu" oncommand="document.getBindingParent(this).showChat(event.target.chat)"/>
   1.270 +        </xul:toolbarbutton>
   1.271 +        <children/>
   1.272 +      </xul:hbox>
   1.273 +    </content>
   1.274 +
   1.275 +    <implementation implements="nsIDOMEventListener">
   1.276 +      <constructor>
   1.277 +        // to avoid reflows we cache the width of the nub.
   1.278 +        this.cachedWidthNub = 0;
   1.279 +        this._selectedChat = null;
   1.280 +      </constructor>
   1.281 +
   1.282 +      <field name="innerbox" readonly="true">
   1.283 +        document.getAnonymousElementByAttribute(this, "anonid", "innerbox");
   1.284 +      </field>
   1.285 +
   1.286 +      <field name="menupopup" readonly="true">
   1.287 +        document.getAnonymousElementByAttribute(this, "anonid", "nubMenu");
   1.288 +      </field>
   1.289 +
   1.290 +      <field name="nub" readonly="true">
   1.291 +        document.getAnonymousElementByAttribute(this, "anonid", "nub");
   1.292 +      </field>
   1.293 +
   1.294 +      <method name="focus">
   1.295 +        <body><![CDATA[
   1.296 +          if (!this.selectedChat)
   1.297 +            return;
   1.298 +          Services.focus.focusedWindow = this.selectedChat.contentWindow;
   1.299 +        ]]></body>
   1.300 +      </method>
   1.301 +
   1.302 +      <method name="_isChatFocused">
   1.303 +        <parameter name="aChatbox"/>
   1.304 +        <body><![CDATA[
   1.305 +          // If there are no XBL bindings for the chat it can't be focused.
   1.306 +          if (!aChatbox.content)
   1.307 +            return false;
   1.308 +          let fw = Services.focus.focusedWindow;
   1.309 +          if (!fw)
   1.310 +            return false;
   1.311 +          // We want to see if the focused window is in the subtree below our browser...
   1.312 +          let containingBrowser = fw.QueryInterface(Ci.nsIInterfaceRequestor)
   1.313 +                                    .getInterface(Ci.nsIWebNavigation)
   1.314 +                                    .QueryInterface(Ci.nsIDocShell)
   1.315 +                                    .chromeEventHandler;
   1.316 +          return containingBrowser == aChatbox.content;
   1.317 +        ]]></body>
   1.318 +      </method>
   1.319 +
   1.320 +      <property name="selectedChat">
   1.321 +        <getter><![CDATA[
   1.322 +          return this._selectedChat;
   1.323 +        ]]></getter>
   1.324 +        <setter><![CDATA[
   1.325 +          // this is pretty horrible, but we:
   1.326 +          // * want to avoid doing touching 'selected' attribute when the
   1.327 +          //   specified chat is already selected.
   1.328 +          // * remove 'activity' attribute on newly selected tab *even if*
   1.329 +          //   newly selected is already selected.
   1.330 +          // * need to handle either current or new being null.
   1.331 +          if (this._selectedChat != val) {
   1.332 +            if (this._selectedChat) {
   1.333 +              this._selectedChat.removeAttribute("selected");
   1.334 +            }
   1.335 +            this._selectedChat = val;
   1.336 +            if (val) {
   1.337 +              this._selectedChat.setAttribute("selected", "true");
   1.338 +            }
   1.339 +          }
   1.340 +          if (val) {
   1.341 +            this._selectedChat.removeAttribute("activity");
   1.342 +          }
   1.343 +        ]]></setter>
   1.344 +      </property>
   1.345 +
   1.346 +      <field name="menuitemMap">new WeakMap()</field>
   1.347 +      <field name="chatboxForURL">new Map();</field>
   1.348 +
   1.349 +      <property name="hasCollapsedChildren">
   1.350 +        <getter><![CDATA[
   1.351 +          return !!this.querySelector("[collapsed]");
   1.352 +        ]]></getter>
   1.353 +      </property>
   1.354 +
   1.355 +      <property name="collapsedChildren">
   1.356 +        <getter><![CDATA[
   1.357 +          // A generator yielding all collapsed chatboxes, in the order in
   1.358 +          // which they should be restored.
   1.359 +          let child = this.lastElementChild;
   1.360 +          while (child) {
   1.361 +            if (child.collapsed)
   1.362 +              yield child;
   1.363 +            child = child.previousElementSibling;
   1.364 +          }
   1.365 +        ]]></getter>
   1.366 +      </property>
   1.367 +
   1.368 +      <property name="visibleChildren">
   1.369 +        <getter><![CDATA[
   1.370 +          // A generator yielding all non-collapsed chatboxes.
   1.371 +          let child = this.firstElementChild;
   1.372 +          while (child) {
   1.373 +            if (!child.collapsed)
   1.374 +              yield child;
   1.375 +            child = child.nextElementSibling;
   1.376 +          }
   1.377 +        ]]></getter>
   1.378 +      </property>
   1.379 +
   1.380 +      <property name="collapsibleChildren">
   1.381 +        <getter><![CDATA[
   1.382 +          // A generator yielding all children which are able to be collapsed
   1.383 +          // in the order in which they should be collapsed.
   1.384 +          // (currently this is all visible ones other than the selected one.)
   1.385 +          for (let child of this.visibleChildren)
   1.386 +            if (child != this.selectedChat)
   1.387 +              yield child;
   1.388 +        ]]></getter>
   1.389 +      </property>
   1.390 +
   1.391 +      <method name="_selectAnotherChat">
   1.392 +        <body><![CDATA[
   1.393 +          // Select a different chat (as the currently selected one is no
   1.394 +          // longer suitable as the selection - maybe it is being minimized or
   1.395 +          // closed.)  We only select non-minimized and non-collapsed chats,
   1.396 +          // and if none are found, set the selectedChat to null.
   1.397 +          // It's possible in the future we will track most-recently-selected
   1.398 +          // chats or similar to find the "best" candidate - for now though
   1.399 +          // the choice is somewhat arbitrary.
   1.400 +          let moveFocus = this.selectedChat && this._isChatFocused(this.selectedChat);
   1.401 +          for (let other of this.children) {
   1.402 +            if (other != this.selectedChat && !other.minimized && !other.collapsed) {
   1.403 +              this.selectedChat = other;
   1.404 +              if (moveFocus)
   1.405 +                this.focus();
   1.406 +              return;
   1.407 +            }
   1.408 +          }
   1.409 +          // can't find another - so set no chat as selected.
   1.410 +          this.selectedChat = null;
   1.411 +        ]]></body>
   1.412 +      </method>
   1.413 +
   1.414 +      <method name="updateTitlebar">
   1.415 +        <parameter name="aChatbox"/>
   1.416 +        <body><![CDATA[
   1.417 +          if (aChatbox.collapsed) {
   1.418 +            let menuitem = this.menuitemMap.get(aChatbox);
   1.419 +            if (aChatbox.getAttribute("activity")) {
   1.420 +              menuitem.setAttribute("activity", true);
   1.421 +              this.nub.setAttribute("activity", true);
   1.422 +            }
   1.423 +            menuitem.setAttribute("label", aChatbox.getAttribute("label"));
   1.424 +            menuitem.setAttribute("image", aChatbox.getAttribute("image"));
   1.425 +          }
   1.426 +        ]]></body>
   1.427 +      </method>
   1.428 +
   1.429 +      <method name="calcTotalWidthOf">
   1.430 +        <parameter name="aElement"/>
   1.431 +        <body><![CDATA[
   1.432 +          let cs = document.defaultView.getComputedStyle(aElement);
   1.433 +          let margins = parseInt(cs.marginLeft) + parseInt(cs.marginRight);
   1.434 +          return aElement.getBoundingClientRect().width + margins;
   1.435 +        ]]></body>
   1.436 +      </method>
   1.437 +
   1.438 +      <method name="getTotalChildWidth">
   1.439 +        <parameter name="aChatbox"/>
   1.440 +        <body><![CDATA[
   1.441 +          // These are from the CSS for the chatbox and must be kept in sync.
   1.442 +          // We can't use calcTotalWidthOf due to the transitions...
   1.443 +          const CHAT_WIDTH_OPEN = 260;
   1.444 +          const CHAT_WIDTH_MINIMIZED = 160;
   1.445 +          return aChatbox.minimized ? CHAT_WIDTH_MINIMIZED : CHAT_WIDTH_OPEN;
   1.446 +        ]]></body>
   1.447 +      </method>
   1.448 +
   1.449 +      <method name="collapseChat">
   1.450 +        <parameter name="aChatbox"/>
   1.451 +        <body><![CDATA[
   1.452 +          // we ensure that the cached width for a child of this type is
   1.453 +          // up-to-date so we can use it when resizing.
   1.454 +          this.getTotalChildWidth(aChatbox);
   1.455 +          aChatbox.collapsed = true;
   1.456 +          aChatbox.isActive = false;
   1.457 +          let menu = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "menuitem");
   1.458 +          menu.setAttribute("class", "menuitem-iconic");
   1.459 +          menu.setAttribute("label", aChatbox.contentDocument.title);
   1.460 +          menu.setAttribute("image", aChatbox.getAttribute("image"));
   1.461 +          menu.chat = aChatbox;
   1.462 +          this.menuitemMap.set(aChatbox, menu);
   1.463 +          this.menupopup.appendChild(menu);
   1.464 +          this.nub.collapsed = false;
   1.465 +        ]]></body>
   1.466 +      </method>
   1.467 +
   1.468 +      <method name="showChat">
   1.469 +        <parameter name="aChatbox"/>
   1.470 +        <parameter name="aMode"/>
   1.471 +        <body><![CDATA[
   1.472 +          if ((aMode != "minimized") && aChatbox.minimized)
   1.473 +            aChatbox.minimized = false;
   1.474 +          if (this.selectedChat != aChatbox)
   1.475 +            this.selectedChat = aChatbox;
   1.476 +          if (!aChatbox.collapsed)
   1.477 +            return; // already showing - no more to do.
   1.478 +          this._showChat(aChatbox);
   1.479 +          // showing a collapsed chat might mean another needs to be collapsed
   1.480 +          // to make room...
   1.481 +          this.resize();
   1.482 +        ]]></body>
   1.483 +      </method>
   1.484 +
   1.485 +      <method name="_showChat">
   1.486 +        <parameter name="aChatbox"/>
   1.487 +        <body><![CDATA[
   1.488 +          // the actual implementation - doesn't check for overflow, assumes
   1.489 +          // collapsed, etc.
   1.490 +          let menuitem = this.menuitemMap.get(aChatbox);
   1.491 +          this.menuitemMap.delete(aChatbox);
   1.492 +          this.menupopup.removeChild(menuitem);
   1.493 +          aChatbox.collapsed = false;
   1.494 +          aChatbox.isActive = !aChatbox.minimized;
   1.495 +        ]]></body>
   1.496 +      </method>
   1.497 +
   1.498 +      <method name="remove">
   1.499 +        <parameter name="aChatbox"/>
   1.500 +        <body><![CDATA[
   1.501 +          this._remove(aChatbox);
   1.502 +          // The removal of a chat may mean a collapsed one can spring up,
   1.503 +          // or that the popup should be hidden.  We also defer the selection
   1.504 +          // of another chat until after a resize, as a new candidate may
   1.505 +          // become uncollapsed after the resize.
   1.506 +          this.resize();
   1.507 +          if (this.selectedChat == aChatbox) {
   1.508 +            this._selectAnotherChat();
   1.509 +          }
   1.510 +        ]]></body>
   1.511 +      </method>
   1.512 +
   1.513 +      <method name="_remove">
   1.514 +        <parameter name="aChatbox"/>
   1.515 +        <body><![CDATA[
   1.516 +          aChatbox.content.socialErrorListener.remove();
   1.517 +          this.removeChild(aChatbox);
   1.518 +          // child might have been collapsed.
   1.519 +          let menuitem = this.menuitemMap.get(aChatbox);
   1.520 +          if (menuitem) {
   1.521 +            this.menuitemMap.delete(aChatbox);
   1.522 +            this.menupopup.removeChild(menuitem);
   1.523 +          }
   1.524 +          this.chatboxForURL.delete(aChatbox.src);
   1.525 +        ]]></body>
   1.526 +      </method>
   1.527 +
   1.528 +      <method name="removeAll">
   1.529 +        <body><![CDATA[
   1.530 +          this.selectedChat = null;
   1.531 +          while (this.firstElementChild) {
   1.532 +            this._remove(this.firstElementChild);
   1.533 +          }
   1.534 +          // and the nub/popup must also die.
   1.535 +          this.nub.collapsed = true;
   1.536 +        ]]></body>
   1.537 +      </method>
   1.538 +
   1.539 +      <method name="openChat">
   1.540 +        <parameter name="aProvider"/>
   1.541 +        <parameter name="aURL"/>
   1.542 +        <parameter name="aCallback"/>
   1.543 +        <parameter name="aMode"/>
   1.544 +        <body><![CDATA[
   1.545 +          let cb = this.chatboxForURL.get(aURL);
   1.546 +          if (cb) {
   1.547 +            cb = cb.get();
   1.548 +            if (cb.parentNode) {
   1.549 +              this.showChat(cb, aMode);
   1.550 +              if (aCallback) {
   1.551 +                if (cb._callbacks == null) {
   1.552 +                  // DOMContentLoaded has already fired, so callback now.
   1.553 +                  aCallback(cb.contentWindow);
   1.554 +                } else {
   1.555 +                  // DOMContentLoaded for this chat is yet to fire...
   1.556 +                  cb._callbacks.push(aCallback);
   1.557 +                }
   1.558 +              }
   1.559 +              return;
   1.560 +            }
   1.561 +            this.chatboxForURL.delete(aURL);
   1.562 +          }
   1.563 +          cb = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "chatbox");
   1.564 +          // _callbacks is a javascript property instead of a <field> as it
   1.565 +          // must exist before the (possibly delayed) bindings are created.
   1.566 +          cb._callbacks = [aCallback];
   1.567 +          // src also a javascript property; the src attribute is set in the ctor.
   1.568 +          cb.src = aURL;
   1.569 +          if (aMode == "minimized")
   1.570 +            cb.setAttribute("minimized", "true");
   1.571 +          cb.setAttribute("origin", aProvider.origin);
   1.572 +          this.insertBefore(cb, this.firstChild);
   1.573 +          this.selectedChat = cb;
   1.574 +          this.chatboxForURL.set(aURL, Cu.getWeakReference(cb));
   1.575 +          this.resize();
   1.576 +        ]]></body>
   1.577 +      </method>
   1.578 +
   1.579 +      <method name="resize">
   1.580 +        <body><![CDATA[
   1.581 +        // Checks the current size against the collapsed state of children
   1.582 +        // and collapses or expands as necessary such that as many as possible
   1.583 +        // are shown.
   1.584 +        // So 2 basic strategies:
   1.585 +        // * Collapse/Expand one at a time until we can't collapse/expand any
   1.586 +        //   more - but this is one reflow per change.
   1.587 +        // * Calculate the dimensions ourself and choose how many to collapse
   1.588 +        //   or expand based on this, then do them all in one go.  This is one
   1.589 +        //   reflow regardless of how many we change.
   1.590 +        // So we go the more complicated but more efficient second option...
   1.591 +        let availWidth = this.getBoundingClientRect().width;
   1.592 +        let currentWidth = 0;
   1.593 +        if (!this.nub.collapsed) { // the nub is visible.
   1.594 +          if (!this.cachedWidthNub)
   1.595 +            this.cachedWidthNub = this.calcTotalWidthOf(this.nub);
   1.596 +          currentWidth += this.cachedWidthNub;
   1.597 +        }
   1.598 +        for (let child of this.visibleChildren) {
   1.599 +          currentWidth += this.getTotalChildWidth(child);
   1.600 +        }
   1.601 +
   1.602 +        if (currentWidth > availWidth) {
   1.603 +          // we need to collapse some.
   1.604 +          let toCollapse = [];
   1.605 +          for (let child of this.collapsibleChildren) {
   1.606 +            if (currentWidth <= availWidth)
   1.607 +              break;
   1.608 +            toCollapse.push(child);
   1.609 +            currentWidth -= this.getTotalChildWidth(child);
   1.610 +          }
   1.611 +          if (toCollapse.length) {
   1.612 +            for (let child of toCollapse)
   1.613 +              this.collapseChat(child);
   1.614 +          }
   1.615 +        } else if (currentWidth < availWidth) {
   1.616 +          // we *might* be able to expand some - see how many.
   1.617 +          // XXX - if this was clever, it could know when removing the nub
   1.618 +          // leaves enough space to show all collapsed
   1.619 +          let toShow = [];
   1.620 +          for (let child of this.collapsedChildren) {
   1.621 +            currentWidth += this.getTotalChildWidth(child);
   1.622 +            if (currentWidth > availWidth)
   1.623 +              break;
   1.624 +            toShow.push(child);
   1.625 +          }
   1.626 +          for (let child of toShow)
   1.627 +            this._showChat(child);
   1.628 +
   1.629 +          // If none remain collapsed remove the nub.
   1.630 +          if (!this.hasCollapsedChildren) {
   1.631 +            this.nub.collapsed = true;
   1.632 +          }
   1.633 +        }
   1.634 +        // else: achievement unlocked - we are pixel-perfect!
   1.635 +        ]]></body>
   1.636 +      </method>
   1.637 +
   1.638 +      <method name="handleEvent">
   1.639 +        <parameter name="aEvent"/>
   1.640 +        <body><![CDATA[
   1.641 +          if (aEvent.type == "resize") {
   1.642 +            this.resize();
   1.643 +          }
   1.644 +        ]]></body>
   1.645 +      </method>
   1.646 +
   1.647 +      <method name="_getDragTarget">
   1.648 +        <parameter name="event"/>
   1.649 +        <body><![CDATA[
   1.650 +          return event.target.localName == "chatbox" ? event.target : null;
   1.651 +        ]]></body>
   1.652 +      </method>
   1.653 +
   1.654 +      <!-- Moves a chatbox to a new window. -->
   1.655 +      <method name="detachChatbox">
   1.656 +        <parameter name="aChatbox"/>
   1.657 +        <parameter name="aOptions"/>
   1.658 +        <parameter name="aCallback"/>
   1.659 +        <body><![CDATA[
   1.660 +          let options = "";
   1.661 +          for (let name in aOptions)
   1.662 +            options += "," + name + "=" + aOptions[name];
   1.663 +
   1.664 +          let otherWin = window.openDialog("chrome://browser/content/chatWindow.xul",
   1.665 +                                           "_blank", "chrome,all,dialog=no" + options);
   1.666 +
   1.667 +          otherWin.addEventListener("load", function _chatLoad(event) {
   1.668 +            if (event.target != otherWin.document)
   1.669 +              return;
   1.670 +
   1.671 +            otherWin.removeEventListener("load", _chatLoad, true);
   1.672 +            let otherChatbox = otherWin.document.getElementById("chatter");
   1.673 +            aChatbox.swapDocShells(otherChatbox);
   1.674 +            aChatbox.close();
   1.675 +            if (aCallback)
   1.676 +              aCallback(otherWin);
   1.677 +          }, true);
   1.678 +        ]]></body>
   1.679 +      </method>
   1.680 +
   1.681 +    </implementation>
   1.682 +
   1.683 +    <handlers>
   1.684 +      <handler event="popupshown"><![CDATA[
   1.685 +        this.nub.removeAttribute("activity");
   1.686 +      ]]></handler>
   1.687 +      <handler event="load"><![CDATA[
   1.688 +        window.addEventListener("resize", this, true);
   1.689 +      ]]></handler>
   1.690 +      <handler event="unload"><![CDATA[
   1.691 +        window.removeEventListener("resize", this, true);
   1.692 +      ]]></handler>
   1.693 +
   1.694 +      <handler event="dragstart"><![CDATA[
   1.695 +        // chat window dragging is essentially duplicated from tabbrowser.xml
   1.696 +        // to acheive the same visual experience
   1.697 +        let chatbox = this._getDragTarget(event);
   1.698 +        if (!chatbox) {
   1.699 +          return;
   1.700 +        }
   1.701 +
   1.702 +        let dt = event.dataTransfer;
   1.703 +        // we do not set a url in the drag data to prevent moving to tabbrowser
   1.704 +        // or otherwise having unexpected drop handlers do something with our
   1.705 +        // chatbox
   1.706 +        dt.mozSetDataAt("application/x-moz-chatbox", chatbox, 0);
   1.707 +
   1.708 +        // Set the cursor to an arrow during tab drags.
   1.709 +        dt.mozCursor = "default";
   1.710 +
   1.711 +        // Create a canvas to which we capture the current tab.
   1.712 +        // Until canvas is HiDPI-aware (bug 780362), we need to scale the desired
   1.713 +        // canvas size (in CSS pixels) to the window's backing resolution in order
   1.714 +        // to get a full-resolution drag image for use on HiDPI displays.
   1.715 +        let windowUtils = window.getInterface(Ci.nsIDOMWindowUtils);
   1.716 +        let scale = windowUtils.screenPixelsPerCSSPixel / windowUtils.fullZoom;
   1.717 +        let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
   1.718 +        canvas.mozOpaque = true;
   1.719 +        canvas.width = 160 * scale;
   1.720 +        canvas.height = 90 * scale;
   1.721 +        PageThumbs.captureToCanvas(chatbox.contentWindow, canvas);
   1.722 +        dt.setDragImage(canvas, -16 * scale, -16 * scale);
   1.723 +
   1.724 +        event.stopPropagation();
   1.725 +      ]]></handler>
   1.726 +
   1.727 +      <handler event="dragend"><![CDATA[
   1.728 +        let dt = event.dataTransfer;
   1.729 +        let draggedChat = dt.mozGetDataAt("application/x-moz-chatbox", 0);
   1.730 +        if (dt.mozUserCancelled || dt.dropEffect != "none") {
   1.731 +          return;
   1.732 +        }
   1.733 +
   1.734 +        let eX = event.screenX;
   1.735 +        let eY = event.screenY;
   1.736 +        // screen.availLeft et. al. only check the screen that this window is on,
   1.737 +        // but we want to look at the screen the tab is being dropped onto.
   1.738 +        let sX = {}, sY = {}, sWidth = {}, sHeight = {};
   1.739 +        Cc["@mozilla.org/gfx/screenmanager;1"]
   1.740 +          .getService(Ci.nsIScreenManager)
   1.741 +          .screenForRect(eX, eY, 1, 1)
   1.742 +          .GetAvailRect(sX, sY, sWidth, sHeight);
   1.743 +        // default size for the chat window as used in chatWindow.xul, use them
   1.744 +        // here to attempt to keep the window fully within the screen when
   1.745 +        // opening at the drop point. If the user has resized the window to
   1.746 +        // something larger (which gets persisted), at least a good portion of
   1.747 +        // the window should still be within the screen.
   1.748 +        let winWidth = 400;
   1.749 +        let winHeight = 420;
   1.750 +        // ensure new window entirely within screen
   1.751 +        let left = Math.min(Math.max(eX, sX.value),
   1.752 +                            sX.value + sWidth.value - winWidth);
   1.753 +        let top = Math.min(Math.max(eY, sY.value),
   1.754 +                           sY.value + sHeight.value - winHeight);
   1.755 +
   1.756 +        let provider = Social._getProviderFromOrigin(draggedChat.content.getAttribute("origin"));
   1.757 +        this.detachChatbox(draggedChat, { screenX: left, screenY: top }, win => {
   1.758 +          win.document.title = provider.name;
   1.759 +        });
   1.760 +
   1.761 +        event.stopPropagation();
   1.762 +      ]]></handler>
   1.763 +    </handlers>
   1.764 +  </binding>
   1.765 +
   1.766 +</bindings>

mercurial