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>