Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
michael@0 | 1 | <?xml version="1.0"?> |
michael@0 | 2 | |
michael@0 | 3 | <bindings id="socialChatBindings" |
michael@0 | 4 | xmlns="http://www.mozilla.org/xbl" |
michael@0 | 5 | xmlns:xbl="http://www.mozilla.org/xbl" |
michael@0 | 6 | xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> |
michael@0 | 7 | |
michael@0 | 8 | <binding id="chatbox"> |
michael@0 | 9 | <content orient="vertical" mousethrough="never"> |
michael@0 | 10 | <xul:hbox class="chat-titlebar" xbl:inherits="minimized,selected,activity" align="baseline"> |
michael@0 | 11 | <xul:hbox flex="1" onclick="document.getBindingParent(this).onTitlebarClick(event);"> |
michael@0 | 12 | <xul:image class="chat-status-icon" xbl:inherits="src=image"/> |
michael@0 | 13 | <xul:label class="chat-title" flex="1" xbl:inherits="value=label" crop="center"/> |
michael@0 | 14 | </xul:hbox> |
michael@0 | 15 | <xul:toolbarbutton anonid="notification-icon" class="notification-anchor-icon chat-toolbarbutton" |
michael@0 | 16 | oncommand="document.getBindingParent(this).showNotifications(); event.stopPropagation();"/> |
michael@0 | 17 | <xul:toolbarbutton anonid="minimize" class="chat-minimize-button chat-toolbarbutton" |
michael@0 | 18 | oncommand="document.getBindingParent(this).toggle();"/> |
michael@0 | 19 | <xul:toolbarbutton anonid="swap" class="chat-swap-button chat-toolbarbutton" |
michael@0 | 20 | oncommand="document.getBindingParent(this).swapWindows();"/> |
michael@0 | 21 | <xul:toolbarbutton anonid="close" class="chat-close-button chat-toolbarbutton" |
michael@0 | 22 | oncommand="document.getBindingParent(this).close();"/> |
michael@0 | 23 | </xul:hbox> |
michael@0 | 24 | <xul:browser anonid="content" class="chat-frame" flex="1" |
michael@0 | 25 | context="contentAreaContextMenu" |
michael@0 | 26 | disableglobalhistory="true" |
michael@0 | 27 | tooltip="aHTMLTooltip" |
michael@0 | 28 | xbl:inherits="src,origin" type="content"/> |
michael@0 | 29 | </content> |
michael@0 | 30 | |
michael@0 | 31 | <implementation implements="nsIDOMEventListener"> |
michael@0 | 32 | <constructor><![CDATA[ |
michael@0 | 33 | let Social = Components.utils.import("resource:///modules/Social.jsm", {}).Social; |
michael@0 | 34 | this.content.__defineGetter__("popupnotificationanchor", |
michael@0 | 35 | () => document.getAnonymousElementByAttribute(this, "anonid", "notification-icon")); |
michael@0 | 36 | Social.setErrorListener(this.content, function(aBrowser) { |
michael@0 | 37 | aBrowser.webNavigation.loadURI("about:socialerror?mode=compactInfo&origin=" + |
michael@0 | 38 | encodeURIComponent(aBrowser.getAttribute("origin")), |
michael@0 | 39 | null, null, null, null); |
michael@0 | 40 | }); |
michael@0 | 41 | if (!this.chatbar) { |
michael@0 | 42 | document.getAnonymousElementByAttribute(this, "anonid", "minimize").hidden = true; |
michael@0 | 43 | document.getAnonymousElementByAttribute(this, "anonid", "close").hidden = true; |
michael@0 | 44 | } |
michael@0 | 45 | let contentWindow = this.contentWindow; |
michael@0 | 46 | this.addEventListener("DOMContentLoaded", function DOMContentLoaded(event) { |
michael@0 | 47 | if (event.target != this.contentDocument) |
michael@0 | 48 | return; |
michael@0 | 49 | this.removeEventListener("DOMContentLoaded", DOMContentLoaded, true); |
michael@0 | 50 | this.isActive = !this.minimized; |
michael@0 | 51 | // process this._callbacks, then set to null so the chatbox creator |
michael@0 | 52 | // knows to make new callbacks immediately. |
michael@0 | 53 | if (this._callbacks) { |
michael@0 | 54 | for (let callback of this._callbacks) { |
michael@0 | 55 | if (callback) |
michael@0 | 56 | callback(contentWindow); |
michael@0 | 57 | } |
michael@0 | 58 | this._callbacks = null; |
michael@0 | 59 | } |
michael@0 | 60 | |
michael@0 | 61 | // content can send a socialChatActivity event to have the UI update. |
michael@0 | 62 | let chatActivity = function() { |
michael@0 | 63 | this.setAttribute("activity", true); |
michael@0 | 64 | if (this.chatbar) |
michael@0 | 65 | this.chatbar.updateTitlebar(this); |
michael@0 | 66 | }.bind(this); |
michael@0 | 67 | contentWindow.addEventListener("socialChatActivity", chatActivity); |
michael@0 | 68 | contentWindow.addEventListener("unload", function unload() { |
michael@0 | 69 | contentWindow.removeEventListener("unload", unload); |
michael@0 | 70 | contentWindow.removeEventListener("socialChatActivity", chatActivity); |
michael@0 | 71 | }); |
michael@0 | 72 | }, true); |
michael@0 | 73 | if (this.src) |
michael@0 | 74 | this.setAttribute("src", this.src); |
michael@0 | 75 | ]]></constructor> |
michael@0 | 76 | |
michael@0 | 77 | <field name="content" readonly="true"> |
michael@0 | 78 | document.getAnonymousElementByAttribute(this, "anonid", "content"); |
michael@0 | 79 | </field> |
michael@0 | 80 | |
michael@0 | 81 | <property name="contentWindow"> |
michael@0 | 82 | <getter> |
michael@0 | 83 | return this.content.contentWindow; |
michael@0 | 84 | </getter> |
michael@0 | 85 | </property> |
michael@0 | 86 | |
michael@0 | 87 | <property name="contentDocument"> |
michael@0 | 88 | <getter> |
michael@0 | 89 | return this.content.contentDocument; |
michael@0 | 90 | </getter> |
michael@0 | 91 | </property> |
michael@0 | 92 | |
michael@0 | 93 | <property name="minimized"> |
michael@0 | 94 | <getter> |
michael@0 | 95 | return this.getAttribute("minimized") == "true"; |
michael@0 | 96 | </getter> |
michael@0 | 97 | <setter><![CDATA[ |
michael@0 | 98 | // Note that this.isActive is set via our transitionend handler so |
michael@0 | 99 | // the content doesn't see intermediate values. |
michael@0 | 100 | let parent = this.chatbar; |
michael@0 | 101 | if (val) { |
michael@0 | 102 | this.setAttribute("minimized", "true"); |
michael@0 | 103 | // If this chat is the selected one a new one needs to be selected. |
michael@0 | 104 | if (parent && parent.selectedChat == this) |
michael@0 | 105 | parent._selectAnotherChat(); |
michael@0 | 106 | } else { |
michael@0 | 107 | this.removeAttribute("minimized"); |
michael@0 | 108 | // this chat gets selected. |
michael@0 | 109 | if (parent) |
michael@0 | 110 | parent.selectedChat = this; |
michael@0 | 111 | } |
michael@0 | 112 | ]]></setter> |
michael@0 | 113 | </property> |
michael@0 | 114 | |
michael@0 | 115 | <property name="chatbar"> |
michael@0 | 116 | <getter> |
michael@0 | 117 | if (this.parentNode.nodeName == "chatbar") |
michael@0 | 118 | return this.parentNode; |
michael@0 | 119 | return null; |
michael@0 | 120 | </getter> |
michael@0 | 121 | </property> |
michael@0 | 122 | |
michael@0 | 123 | <property name="isActive"> |
michael@0 | 124 | <getter> |
michael@0 | 125 | return this.content.docShell.isActive; |
michael@0 | 126 | </getter> |
michael@0 | 127 | <setter> |
michael@0 | 128 | this.content.docShell.isActive = !!val; |
michael@0 | 129 | |
michael@0 | 130 | // let the chat frame know if it is being shown or hidden |
michael@0 | 131 | let evt = this.contentDocument.createEvent("CustomEvent"); |
michael@0 | 132 | evt.initCustomEvent(val ? "socialFrameShow" : "socialFrameHide", true, true, {}); |
michael@0 | 133 | this.contentDocument.documentElement.dispatchEvent(evt); |
michael@0 | 134 | </setter> |
michael@0 | 135 | </property> |
michael@0 | 136 | |
michael@0 | 137 | <method name="showNotifications"> |
michael@0 | 138 | <body><![CDATA[ |
michael@0 | 139 | PopupNotifications._reshowNotifications(this.content.popupnotificationanchor, |
michael@0 | 140 | this.content); |
michael@0 | 141 | ]]></body> |
michael@0 | 142 | </method> |
michael@0 | 143 | |
michael@0 | 144 | <method name="swapDocShells"> |
michael@0 | 145 | <parameter name="aTarget"/> |
michael@0 | 146 | <body><![CDATA[ |
michael@0 | 147 | aTarget.setAttribute('label', this.contentDocument.title); |
michael@0 | 148 | aTarget.src = this.src; |
michael@0 | 149 | aTarget.content.setAttribute("origin", this.content.getAttribute("origin")); |
michael@0 | 150 | aTarget.content.popupnotificationanchor.className = this.content.popupnotificationanchor.className; |
michael@0 | 151 | this.content.socialErrorListener.remove(); |
michael@0 | 152 | aTarget.content.socialErrorListener.remove(); |
michael@0 | 153 | this.content.swapDocShells(aTarget.content); |
michael@0 | 154 | Social.setErrorListener(this.content, function(aBrowser) {}); // 'this' will be destroyed soon. |
michael@0 | 155 | Social.setErrorListener(aTarget.content, function(aBrowser) { |
michael@0 | 156 | aBrowser.webNavigation.loadURI("about:socialerror?mode=compactInfo&origin=" + |
michael@0 | 157 | encodeURIComponent(aBrowser.getAttribute("origin")), |
michael@0 | 158 | null, null, null, null); |
michael@0 | 159 | }); |
michael@0 | 160 | ]]></body> |
michael@0 | 161 | </method> |
michael@0 | 162 | |
michael@0 | 163 | <method name="onTitlebarClick"> |
michael@0 | 164 | <parameter name="aEvent"/> |
michael@0 | 165 | <body><![CDATA[ |
michael@0 | 166 | if (!this.chatbar) |
michael@0 | 167 | return; |
michael@0 | 168 | if (aEvent.button == 0) { // left-click: toggle minimized. |
michael@0 | 169 | this.toggle(); |
michael@0 | 170 | // if we restored it, we want to focus it. |
michael@0 | 171 | if (!this.minimized) |
michael@0 | 172 | this.chatbar.focus(); |
michael@0 | 173 | } else if (aEvent.button == 1) // middle-click: close chat |
michael@0 | 174 | this.close(); |
michael@0 | 175 | ]]></body> |
michael@0 | 176 | </method> |
michael@0 | 177 | |
michael@0 | 178 | <method name="close"> |
michael@0 | 179 | <body><![CDATA[ |
michael@0 | 180 | if (this.chatbar) |
michael@0 | 181 | this.chatbar.remove(this); |
michael@0 | 182 | else |
michael@0 | 183 | window.close(); |
michael@0 | 184 | ]]></body> |
michael@0 | 185 | </method> |
michael@0 | 186 | |
michael@0 | 187 | <method name="swapWindows"> |
michael@0 | 188 | <body><![CDATA[ |
michael@0 | 189 | let provider = Social._getProviderFromOrigin(this.content.getAttribute("origin")); |
michael@0 | 190 | if (this.chatbar) { |
michael@0 | 191 | this.chatbar.detachChatbox(this, { "centerscreen": "yes" }, win => { |
michael@0 | 192 | win.document.title = provider.name; |
michael@0 | 193 | }); |
michael@0 | 194 | } else { |
michael@0 | 195 | // attach this chatbox to the topmost browser window |
michael@0 | 196 | let findChromeWindowForChats = Cu.import("resource://gre/modules/MozSocialAPI.jsm").findChromeWindowForChats; |
michael@0 | 197 | let win = findChromeWindowForChats(); |
michael@0 | 198 | let chatbar = win.SocialChatBar.chatbar; |
michael@0 | 199 | chatbar.openChat(provider, "about:blank", win => { |
michael@0 | 200 | let cb = chatbar.selectedChat; |
michael@0 | 201 | this.swapDocShells(cb); |
michael@0 | 202 | |
michael@0 | 203 | // chatboxForURL is a map of URL -> chatbox used to avoid opening |
michael@0 | 204 | // duplicate chat windows. Ensure reattached chat windows aren't |
michael@0 | 205 | // registered with about:blank as their URL, otherwise reattaching |
michael@0 | 206 | // more than one chat window isn't possible. |
michael@0 | 207 | chatbar.chatboxForURL.delete("about:blank"); |
michael@0 | 208 | chatbar.chatboxForURL.set(this.src, Cu.getWeakReference(cb)); |
michael@0 | 209 | |
michael@0 | 210 | chatbar.focus(); |
michael@0 | 211 | this.close(); |
michael@0 | 212 | }); |
michael@0 | 213 | } |
michael@0 | 214 | ]]></body> |
michael@0 | 215 | </method> |
michael@0 | 216 | |
michael@0 | 217 | <method name="toggle"> |
michael@0 | 218 | <body><![CDATA[ |
michael@0 | 219 | this.minimized = !this.minimized; |
michael@0 | 220 | ]]></body> |
michael@0 | 221 | </method> |
michael@0 | 222 | </implementation> |
michael@0 | 223 | |
michael@0 | 224 | <handlers> |
michael@0 | 225 | <handler event="focus" phase="capturing"> |
michael@0 | 226 | if (this.chatbar) |
michael@0 | 227 | this.chatbar.selectedChat = this; |
michael@0 | 228 | </handler> |
michael@0 | 229 | <handler event="DOMTitleChanged"><![CDATA[ |
michael@0 | 230 | this.setAttribute('label', this.contentDocument.title); |
michael@0 | 231 | if (this.chatbar) |
michael@0 | 232 | this.chatbar.updateTitlebar(this); |
michael@0 | 233 | ]]></handler> |
michael@0 | 234 | <handler event="DOMLinkAdded"><![CDATA[ |
michael@0 | 235 | // much of this logic is from DOMLinkHandler in browser.js |
michael@0 | 236 | // this sets the presence icon for a chat user, we simply use favicon style updating |
michael@0 | 237 | let link = event.originalTarget; |
michael@0 | 238 | let rel = link.rel && link.rel.toLowerCase(); |
michael@0 | 239 | if (!link || !link.ownerDocument || !rel || !link.href) |
michael@0 | 240 | return; |
michael@0 | 241 | if (link.rel.indexOf("icon") < 0) |
michael@0 | 242 | return; |
michael@0 | 243 | |
michael@0 | 244 | let ContentLinkHandler = Cu.import("resource:///modules/ContentLinkHandler.jsm", {}).ContentLinkHandler; |
michael@0 | 245 | let uri = ContentLinkHandler.getLinkIconURI(link); |
michael@0 | 246 | if (!uri) |
michael@0 | 247 | return; |
michael@0 | 248 | |
michael@0 | 249 | // we made it this far, use it |
michael@0 | 250 | this.setAttribute('image', uri.spec); |
michael@0 | 251 | if (this.chatbar) |
michael@0 | 252 | this.chatbar.updateTitlebar(this); |
michael@0 | 253 | ]]></handler> |
michael@0 | 254 | <handler event="transitionend"> |
michael@0 | 255 | if (this.isActive == this.minimized) |
michael@0 | 256 | this.isActive = !this.minimized; |
michael@0 | 257 | </handler> |
michael@0 | 258 | </handlers> |
michael@0 | 259 | </binding> |
michael@0 | 260 | |
michael@0 | 261 | <binding id="chatbar"> |
michael@0 | 262 | <content> |
michael@0 | 263 | <xul:hbox align="end" pack="end" anonid="innerbox" class="chatbar-innerbox" mousethrough="always" flex="1"> |
michael@0 | 264 | <xul:spacer flex="1" anonid="spacer" class="chatbar-overflow-spacer"/> |
michael@0 | 265 | <xul:toolbarbutton anonid="nub" class="chatbar-button" type="menu" collapsed="true" mousethrough="never"> |
michael@0 | 266 | <xul:menupopup anonid="nubMenu" oncommand="document.getBindingParent(this).showChat(event.target.chat)"/> |
michael@0 | 267 | </xul:toolbarbutton> |
michael@0 | 268 | <children/> |
michael@0 | 269 | </xul:hbox> |
michael@0 | 270 | </content> |
michael@0 | 271 | |
michael@0 | 272 | <implementation implements="nsIDOMEventListener"> |
michael@0 | 273 | <constructor> |
michael@0 | 274 | // to avoid reflows we cache the width of the nub. |
michael@0 | 275 | this.cachedWidthNub = 0; |
michael@0 | 276 | this._selectedChat = null; |
michael@0 | 277 | </constructor> |
michael@0 | 278 | |
michael@0 | 279 | <field name="innerbox" readonly="true"> |
michael@0 | 280 | document.getAnonymousElementByAttribute(this, "anonid", "innerbox"); |
michael@0 | 281 | </field> |
michael@0 | 282 | |
michael@0 | 283 | <field name="menupopup" readonly="true"> |
michael@0 | 284 | document.getAnonymousElementByAttribute(this, "anonid", "nubMenu"); |
michael@0 | 285 | </field> |
michael@0 | 286 | |
michael@0 | 287 | <field name="nub" readonly="true"> |
michael@0 | 288 | document.getAnonymousElementByAttribute(this, "anonid", "nub"); |
michael@0 | 289 | </field> |
michael@0 | 290 | |
michael@0 | 291 | <method name="focus"> |
michael@0 | 292 | <body><![CDATA[ |
michael@0 | 293 | if (!this.selectedChat) |
michael@0 | 294 | return; |
michael@0 | 295 | Services.focus.focusedWindow = this.selectedChat.contentWindow; |
michael@0 | 296 | ]]></body> |
michael@0 | 297 | </method> |
michael@0 | 298 | |
michael@0 | 299 | <method name="_isChatFocused"> |
michael@0 | 300 | <parameter name="aChatbox"/> |
michael@0 | 301 | <body><![CDATA[ |
michael@0 | 302 | // If there are no XBL bindings for the chat it can't be focused. |
michael@0 | 303 | if (!aChatbox.content) |
michael@0 | 304 | return false; |
michael@0 | 305 | let fw = Services.focus.focusedWindow; |
michael@0 | 306 | if (!fw) |
michael@0 | 307 | return false; |
michael@0 | 308 | // We want to see if the focused window is in the subtree below our browser... |
michael@0 | 309 | let containingBrowser = fw.QueryInterface(Ci.nsIInterfaceRequestor) |
michael@0 | 310 | .getInterface(Ci.nsIWebNavigation) |
michael@0 | 311 | .QueryInterface(Ci.nsIDocShell) |
michael@0 | 312 | .chromeEventHandler; |
michael@0 | 313 | return containingBrowser == aChatbox.content; |
michael@0 | 314 | ]]></body> |
michael@0 | 315 | </method> |
michael@0 | 316 | |
michael@0 | 317 | <property name="selectedChat"> |
michael@0 | 318 | <getter><![CDATA[ |
michael@0 | 319 | return this._selectedChat; |
michael@0 | 320 | ]]></getter> |
michael@0 | 321 | <setter><![CDATA[ |
michael@0 | 322 | // this is pretty horrible, but we: |
michael@0 | 323 | // * want to avoid doing touching 'selected' attribute when the |
michael@0 | 324 | // specified chat is already selected. |
michael@0 | 325 | // * remove 'activity' attribute on newly selected tab *even if* |
michael@0 | 326 | // newly selected is already selected. |
michael@0 | 327 | // * need to handle either current or new being null. |
michael@0 | 328 | if (this._selectedChat != val) { |
michael@0 | 329 | if (this._selectedChat) { |
michael@0 | 330 | this._selectedChat.removeAttribute("selected"); |
michael@0 | 331 | } |
michael@0 | 332 | this._selectedChat = val; |
michael@0 | 333 | if (val) { |
michael@0 | 334 | this._selectedChat.setAttribute("selected", "true"); |
michael@0 | 335 | } |
michael@0 | 336 | } |
michael@0 | 337 | if (val) { |
michael@0 | 338 | this._selectedChat.removeAttribute("activity"); |
michael@0 | 339 | } |
michael@0 | 340 | ]]></setter> |
michael@0 | 341 | </property> |
michael@0 | 342 | |
michael@0 | 343 | <field name="menuitemMap">new WeakMap()</field> |
michael@0 | 344 | <field name="chatboxForURL">new Map();</field> |
michael@0 | 345 | |
michael@0 | 346 | <property name="hasCollapsedChildren"> |
michael@0 | 347 | <getter><![CDATA[ |
michael@0 | 348 | return !!this.querySelector("[collapsed]"); |
michael@0 | 349 | ]]></getter> |
michael@0 | 350 | </property> |
michael@0 | 351 | |
michael@0 | 352 | <property name="collapsedChildren"> |
michael@0 | 353 | <getter><![CDATA[ |
michael@0 | 354 | // A generator yielding all collapsed chatboxes, in the order in |
michael@0 | 355 | // which they should be restored. |
michael@0 | 356 | let child = this.lastElementChild; |
michael@0 | 357 | while (child) { |
michael@0 | 358 | if (child.collapsed) |
michael@0 | 359 | yield child; |
michael@0 | 360 | child = child.previousElementSibling; |
michael@0 | 361 | } |
michael@0 | 362 | ]]></getter> |
michael@0 | 363 | </property> |
michael@0 | 364 | |
michael@0 | 365 | <property name="visibleChildren"> |
michael@0 | 366 | <getter><![CDATA[ |
michael@0 | 367 | // A generator yielding all non-collapsed chatboxes. |
michael@0 | 368 | let child = this.firstElementChild; |
michael@0 | 369 | while (child) { |
michael@0 | 370 | if (!child.collapsed) |
michael@0 | 371 | yield child; |
michael@0 | 372 | child = child.nextElementSibling; |
michael@0 | 373 | } |
michael@0 | 374 | ]]></getter> |
michael@0 | 375 | </property> |
michael@0 | 376 | |
michael@0 | 377 | <property name="collapsibleChildren"> |
michael@0 | 378 | <getter><![CDATA[ |
michael@0 | 379 | // A generator yielding all children which are able to be collapsed |
michael@0 | 380 | // in the order in which they should be collapsed. |
michael@0 | 381 | // (currently this is all visible ones other than the selected one.) |
michael@0 | 382 | for (let child of this.visibleChildren) |
michael@0 | 383 | if (child != this.selectedChat) |
michael@0 | 384 | yield child; |
michael@0 | 385 | ]]></getter> |
michael@0 | 386 | </property> |
michael@0 | 387 | |
michael@0 | 388 | <method name="_selectAnotherChat"> |
michael@0 | 389 | <body><![CDATA[ |
michael@0 | 390 | // Select a different chat (as the currently selected one is no |
michael@0 | 391 | // longer suitable as the selection - maybe it is being minimized or |
michael@0 | 392 | // closed.) We only select non-minimized and non-collapsed chats, |
michael@0 | 393 | // and if none are found, set the selectedChat to null. |
michael@0 | 394 | // It's possible in the future we will track most-recently-selected |
michael@0 | 395 | // chats or similar to find the "best" candidate - for now though |
michael@0 | 396 | // the choice is somewhat arbitrary. |
michael@0 | 397 | let moveFocus = this.selectedChat && this._isChatFocused(this.selectedChat); |
michael@0 | 398 | for (let other of this.children) { |
michael@0 | 399 | if (other != this.selectedChat && !other.minimized && !other.collapsed) { |
michael@0 | 400 | this.selectedChat = other; |
michael@0 | 401 | if (moveFocus) |
michael@0 | 402 | this.focus(); |
michael@0 | 403 | return; |
michael@0 | 404 | } |
michael@0 | 405 | } |
michael@0 | 406 | // can't find another - so set no chat as selected. |
michael@0 | 407 | this.selectedChat = null; |
michael@0 | 408 | ]]></body> |
michael@0 | 409 | </method> |
michael@0 | 410 | |
michael@0 | 411 | <method name="updateTitlebar"> |
michael@0 | 412 | <parameter name="aChatbox"/> |
michael@0 | 413 | <body><![CDATA[ |
michael@0 | 414 | if (aChatbox.collapsed) { |
michael@0 | 415 | let menuitem = this.menuitemMap.get(aChatbox); |
michael@0 | 416 | if (aChatbox.getAttribute("activity")) { |
michael@0 | 417 | menuitem.setAttribute("activity", true); |
michael@0 | 418 | this.nub.setAttribute("activity", true); |
michael@0 | 419 | } |
michael@0 | 420 | menuitem.setAttribute("label", aChatbox.getAttribute("label")); |
michael@0 | 421 | menuitem.setAttribute("image", aChatbox.getAttribute("image")); |
michael@0 | 422 | } |
michael@0 | 423 | ]]></body> |
michael@0 | 424 | </method> |
michael@0 | 425 | |
michael@0 | 426 | <method name="calcTotalWidthOf"> |
michael@0 | 427 | <parameter name="aElement"/> |
michael@0 | 428 | <body><![CDATA[ |
michael@0 | 429 | let cs = document.defaultView.getComputedStyle(aElement); |
michael@0 | 430 | let margins = parseInt(cs.marginLeft) + parseInt(cs.marginRight); |
michael@0 | 431 | return aElement.getBoundingClientRect().width + margins; |
michael@0 | 432 | ]]></body> |
michael@0 | 433 | </method> |
michael@0 | 434 | |
michael@0 | 435 | <method name="getTotalChildWidth"> |
michael@0 | 436 | <parameter name="aChatbox"/> |
michael@0 | 437 | <body><![CDATA[ |
michael@0 | 438 | // These are from the CSS for the chatbox and must be kept in sync. |
michael@0 | 439 | // We can't use calcTotalWidthOf due to the transitions... |
michael@0 | 440 | const CHAT_WIDTH_OPEN = 260; |
michael@0 | 441 | const CHAT_WIDTH_MINIMIZED = 160; |
michael@0 | 442 | return aChatbox.minimized ? CHAT_WIDTH_MINIMIZED : CHAT_WIDTH_OPEN; |
michael@0 | 443 | ]]></body> |
michael@0 | 444 | </method> |
michael@0 | 445 | |
michael@0 | 446 | <method name="collapseChat"> |
michael@0 | 447 | <parameter name="aChatbox"/> |
michael@0 | 448 | <body><![CDATA[ |
michael@0 | 449 | // we ensure that the cached width for a child of this type is |
michael@0 | 450 | // up-to-date so we can use it when resizing. |
michael@0 | 451 | this.getTotalChildWidth(aChatbox); |
michael@0 | 452 | aChatbox.collapsed = true; |
michael@0 | 453 | aChatbox.isActive = false; |
michael@0 | 454 | let menu = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "menuitem"); |
michael@0 | 455 | menu.setAttribute("class", "menuitem-iconic"); |
michael@0 | 456 | menu.setAttribute("label", aChatbox.contentDocument.title); |
michael@0 | 457 | menu.setAttribute("image", aChatbox.getAttribute("image")); |
michael@0 | 458 | menu.chat = aChatbox; |
michael@0 | 459 | this.menuitemMap.set(aChatbox, menu); |
michael@0 | 460 | this.menupopup.appendChild(menu); |
michael@0 | 461 | this.nub.collapsed = false; |
michael@0 | 462 | ]]></body> |
michael@0 | 463 | </method> |
michael@0 | 464 | |
michael@0 | 465 | <method name="showChat"> |
michael@0 | 466 | <parameter name="aChatbox"/> |
michael@0 | 467 | <parameter name="aMode"/> |
michael@0 | 468 | <body><![CDATA[ |
michael@0 | 469 | if ((aMode != "minimized") && aChatbox.minimized) |
michael@0 | 470 | aChatbox.minimized = false; |
michael@0 | 471 | if (this.selectedChat != aChatbox) |
michael@0 | 472 | this.selectedChat = aChatbox; |
michael@0 | 473 | if (!aChatbox.collapsed) |
michael@0 | 474 | return; // already showing - no more to do. |
michael@0 | 475 | this._showChat(aChatbox); |
michael@0 | 476 | // showing a collapsed chat might mean another needs to be collapsed |
michael@0 | 477 | // to make room... |
michael@0 | 478 | this.resize(); |
michael@0 | 479 | ]]></body> |
michael@0 | 480 | </method> |
michael@0 | 481 | |
michael@0 | 482 | <method name="_showChat"> |
michael@0 | 483 | <parameter name="aChatbox"/> |
michael@0 | 484 | <body><![CDATA[ |
michael@0 | 485 | // the actual implementation - doesn't check for overflow, assumes |
michael@0 | 486 | // collapsed, etc. |
michael@0 | 487 | let menuitem = this.menuitemMap.get(aChatbox); |
michael@0 | 488 | this.menuitemMap.delete(aChatbox); |
michael@0 | 489 | this.menupopup.removeChild(menuitem); |
michael@0 | 490 | aChatbox.collapsed = false; |
michael@0 | 491 | aChatbox.isActive = !aChatbox.minimized; |
michael@0 | 492 | ]]></body> |
michael@0 | 493 | </method> |
michael@0 | 494 | |
michael@0 | 495 | <method name="remove"> |
michael@0 | 496 | <parameter name="aChatbox"/> |
michael@0 | 497 | <body><![CDATA[ |
michael@0 | 498 | this._remove(aChatbox); |
michael@0 | 499 | // The removal of a chat may mean a collapsed one can spring up, |
michael@0 | 500 | // or that the popup should be hidden. We also defer the selection |
michael@0 | 501 | // of another chat until after a resize, as a new candidate may |
michael@0 | 502 | // become uncollapsed after the resize. |
michael@0 | 503 | this.resize(); |
michael@0 | 504 | if (this.selectedChat == aChatbox) { |
michael@0 | 505 | this._selectAnotherChat(); |
michael@0 | 506 | } |
michael@0 | 507 | ]]></body> |
michael@0 | 508 | </method> |
michael@0 | 509 | |
michael@0 | 510 | <method name="_remove"> |
michael@0 | 511 | <parameter name="aChatbox"/> |
michael@0 | 512 | <body><![CDATA[ |
michael@0 | 513 | aChatbox.content.socialErrorListener.remove(); |
michael@0 | 514 | this.removeChild(aChatbox); |
michael@0 | 515 | // child might have been collapsed. |
michael@0 | 516 | let menuitem = this.menuitemMap.get(aChatbox); |
michael@0 | 517 | if (menuitem) { |
michael@0 | 518 | this.menuitemMap.delete(aChatbox); |
michael@0 | 519 | this.menupopup.removeChild(menuitem); |
michael@0 | 520 | } |
michael@0 | 521 | this.chatboxForURL.delete(aChatbox.src); |
michael@0 | 522 | ]]></body> |
michael@0 | 523 | </method> |
michael@0 | 524 | |
michael@0 | 525 | <method name="removeAll"> |
michael@0 | 526 | <body><![CDATA[ |
michael@0 | 527 | this.selectedChat = null; |
michael@0 | 528 | while (this.firstElementChild) { |
michael@0 | 529 | this._remove(this.firstElementChild); |
michael@0 | 530 | } |
michael@0 | 531 | // and the nub/popup must also die. |
michael@0 | 532 | this.nub.collapsed = true; |
michael@0 | 533 | ]]></body> |
michael@0 | 534 | </method> |
michael@0 | 535 | |
michael@0 | 536 | <method name="openChat"> |
michael@0 | 537 | <parameter name="aProvider"/> |
michael@0 | 538 | <parameter name="aURL"/> |
michael@0 | 539 | <parameter name="aCallback"/> |
michael@0 | 540 | <parameter name="aMode"/> |
michael@0 | 541 | <body><![CDATA[ |
michael@0 | 542 | let cb = this.chatboxForURL.get(aURL); |
michael@0 | 543 | if (cb) { |
michael@0 | 544 | cb = cb.get(); |
michael@0 | 545 | if (cb.parentNode) { |
michael@0 | 546 | this.showChat(cb, aMode); |
michael@0 | 547 | if (aCallback) { |
michael@0 | 548 | if (cb._callbacks == null) { |
michael@0 | 549 | // DOMContentLoaded has already fired, so callback now. |
michael@0 | 550 | aCallback(cb.contentWindow); |
michael@0 | 551 | } else { |
michael@0 | 552 | // DOMContentLoaded for this chat is yet to fire... |
michael@0 | 553 | cb._callbacks.push(aCallback); |
michael@0 | 554 | } |
michael@0 | 555 | } |
michael@0 | 556 | return; |
michael@0 | 557 | } |
michael@0 | 558 | this.chatboxForURL.delete(aURL); |
michael@0 | 559 | } |
michael@0 | 560 | cb = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "chatbox"); |
michael@0 | 561 | // _callbacks is a javascript property instead of a <field> as it |
michael@0 | 562 | // must exist before the (possibly delayed) bindings are created. |
michael@0 | 563 | cb._callbacks = [aCallback]; |
michael@0 | 564 | // src also a javascript property; the src attribute is set in the ctor. |
michael@0 | 565 | cb.src = aURL; |
michael@0 | 566 | if (aMode == "minimized") |
michael@0 | 567 | cb.setAttribute("minimized", "true"); |
michael@0 | 568 | cb.setAttribute("origin", aProvider.origin); |
michael@0 | 569 | this.insertBefore(cb, this.firstChild); |
michael@0 | 570 | this.selectedChat = cb; |
michael@0 | 571 | this.chatboxForURL.set(aURL, Cu.getWeakReference(cb)); |
michael@0 | 572 | this.resize(); |
michael@0 | 573 | ]]></body> |
michael@0 | 574 | </method> |
michael@0 | 575 | |
michael@0 | 576 | <method name="resize"> |
michael@0 | 577 | <body><![CDATA[ |
michael@0 | 578 | // Checks the current size against the collapsed state of children |
michael@0 | 579 | // and collapses or expands as necessary such that as many as possible |
michael@0 | 580 | // are shown. |
michael@0 | 581 | // So 2 basic strategies: |
michael@0 | 582 | // * Collapse/Expand one at a time until we can't collapse/expand any |
michael@0 | 583 | // more - but this is one reflow per change. |
michael@0 | 584 | // * Calculate the dimensions ourself and choose how many to collapse |
michael@0 | 585 | // or expand based on this, then do them all in one go. This is one |
michael@0 | 586 | // reflow regardless of how many we change. |
michael@0 | 587 | // So we go the more complicated but more efficient second option... |
michael@0 | 588 | let availWidth = this.getBoundingClientRect().width; |
michael@0 | 589 | let currentWidth = 0; |
michael@0 | 590 | if (!this.nub.collapsed) { // the nub is visible. |
michael@0 | 591 | if (!this.cachedWidthNub) |
michael@0 | 592 | this.cachedWidthNub = this.calcTotalWidthOf(this.nub); |
michael@0 | 593 | currentWidth += this.cachedWidthNub; |
michael@0 | 594 | } |
michael@0 | 595 | for (let child of this.visibleChildren) { |
michael@0 | 596 | currentWidth += this.getTotalChildWidth(child); |
michael@0 | 597 | } |
michael@0 | 598 | |
michael@0 | 599 | if (currentWidth > availWidth) { |
michael@0 | 600 | // we need to collapse some. |
michael@0 | 601 | let toCollapse = []; |
michael@0 | 602 | for (let child of this.collapsibleChildren) { |
michael@0 | 603 | if (currentWidth <= availWidth) |
michael@0 | 604 | break; |
michael@0 | 605 | toCollapse.push(child); |
michael@0 | 606 | currentWidth -= this.getTotalChildWidth(child); |
michael@0 | 607 | } |
michael@0 | 608 | if (toCollapse.length) { |
michael@0 | 609 | for (let child of toCollapse) |
michael@0 | 610 | this.collapseChat(child); |
michael@0 | 611 | } |
michael@0 | 612 | } else if (currentWidth < availWidth) { |
michael@0 | 613 | // we *might* be able to expand some - see how many. |
michael@0 | 614 | // XXX - if this was clever, it could know when removing the nub |
michael@0 | 615 | // leaves enough space to show all collapsed |
michael@0 | 616 | let toShow = []; |
michael@0 | 617 | for (let child of this.collapsedChildren) { |
michael@0 | 618 | currentWidth += this.getTotalChildWidth(child); |
michael@0 | 619 | if (currentWidth > availWidth) |
michael@0 | 620 | break; |
michael@0 | 621 | toShow.push(child); |
michael@0 | 622 | } |
michael@0 | 623 | for (let child of toShow) |
michael@0 | 624 | this._showChat(child); |
michael@0 | 625 | |
michael@0 | 626 | // If none remain collapsed remove the nub. |
michael@0 | 627 | if (!this.hasCollapsedChildren) { |
michael@0 | 628 | this.nub.collapsed = true; |
michael@0 | 629 | } |
michael@0 | 630 | } |
michael@0 | 631 | // else: achievement unlocked - we are pixel-perfect! |
michael@0 | 632 | ]]></body> |
michael@0 | 633 | </method> |
michael@0 | 634 | |
michael@0 | 635 | <method name="handleEvent"> |
michael@0 | 636 | <parameter name="aEvent"/> |
michael@0 | 637 | <body><![CDATA[ |
michael@0 | 638 | if (aEvent.type == "resize") { |
michael@0 | 639 | this.resize(); |
michael@0 | 640 | } |
michael@0 | 641 | ]]></body> |
michael@0 | 642 | </method> |
michael@0 | 643 | |
michael@0 | 644 | <method name="_getDragTarget"> |
michael@0 | 645 | <parameter name="event"/> |
michael@0 | 646 | <body><![CDATA[ |
michael@0 | 647 | return event.target.localName == "chatbox" ? event.target : null; |
michael@0 | 648 | ]]></body> |
michael@0 | 649 | </method> |
michael@0 | 650 | |
michael@0 | 651 | <!-- Moves a chatbox to a new window. --> |
michael@0 | 652 | <method name="detachChatbox"> |
michael@0 | 653 | <parameter name="aChatbox"/> |
michael@0 | 654 | <parameter name="aOptions"/> |
michael@0 | 655 | <parameter name="aCallback"/> |
michael@0 | 656 | <body><![CDATA[ |
michael@0 | 657 | let options = ""; |
michael@0 | 658 | for (let name in aOptions) |
michael@0 | 659 | options += "," + name + "=" + aOptions[name]; |
michael@0 | 660 | |
michael@0 | 661 | let otherWin = window.openDialog("chrome://browser/content/chatWindow.xul", |
michael@0 | 662 | "_blank", "chrome,all,dialog=no" + options); |
michael@0 | 663 | |
michael@0 | 664 | otherWin.addEventListener("load", function _chatLoad(event) { |
michael@0 | 665 | if (event.target != otherWin.document) |
michael@0 | 666 | return; |
michael@0 | 667 | |
michael@0 | 668 | otherWin.removeEventListener("load", _chatLoad, true); |
michael@0 | 669 | let otherChatbox = otherWin.document.getElementById("chatter"); |
michael@0 | 670 | aChatbox.swapDocShells(otherChatbox); |
michael@0 | 671 | aChatbox.close(); |
michael@0 | 672 | if (aCallback) |
michael@0 | 673 | aCallback(otherWin); |
michael@0 | 674 | }, true); |
michael@0 | 675 | ]]></body> |
michael@0 | 676 | </method> |
michael@0 | 677 | |
michael@0 | 678 | </implementation> |
michael@0 | 679 | |
michael@0 | 680 | <handlers> |
michael@0 | 681 | <handler event="popupshown"><![CDATA[ |
michael@0 | 682 | this.nub.removeAttribute("activity"); |
michael@0 | 683 | ]]></handler> |
michael@0 | 684 | <handler event="load"><![CDATA[ |
michael@0 | 685 | window.addEventListener("resize", this, true); |
michael@0 | 686 | ]]></handler> |
michael@0 | 687 | <handler event="unload"><![CDATA[ |
michael@0 | 688 | window.removeEventListener("resize", this, true); |
michael@0 | 689 | ]]></handler> |
michael@0 | 690 | |
michael@0 | 691 | <handler event="dragstart"><![CDATA[ |
michael@0 | 692 | // chat window dragging is essentially duplicated from tabbrowser.xml |
michael@0 | 693 | // to acheive the same visual experience |
michael@0 | 694 | let chatbox = this._getDragTarget(event); |
michael@0 | 695 | if (!chatbox) { |
michael@0 | 696 | return; |
michael@0 | 697 | } |
michael@0 | 698 | |
michael@0 | 699 | let dt = event.dataTransfer; |
michael@0 | 700 | // we do not set a url in the drag data to prevent moving to tabbrowser |
michael@0 | 701 | // or otherwise having unexpected drop handlers do something with our |
michael@0 | 702 | // chatbox |
michael@0 | 703 | dt.mozSetDataAt("application/x-moz-chatbox", chatbox, 0); |
michael@0 | 704 | |
michael@0 | 705 | // Set the cursor to an arrow during tab drags. |
michael@0 | 706 | dt.mozCursor = "default"; |
michael@0 | 707 | |
michael@0 | 708 | // Create a canvas to which we capture the current tab. |
michael@0 | 709 | // Until canvas is HiDPI-aware (bug 780362), we need to scale the desired |
michael@0 | 710 | // canvas size (in CSS pixels) to the window's backing resolution in order |
michael@0 | 711 | // to get a full-resolution drag image for use on HiDPI displays. |
michael@0 | 712 | let windowUtils = window.getInterface(Ci.nsIDOMWindowUtils); |
michael@0 | 713 | let scale = windowUtils.screenPixelsPerCSSPixel / windowUtils.fullZoom; |
michael@0 | 714 | let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas"); |
michael@0 | 715 | canvas.mozOpaque = true; |
michael@0 | 716 | canvas.width = 160 * scale; |
michael@0 | 717 | canvas.height = 90 * scale; |
michael@0 | 718 | PageThumbs.captureToCanvas(chatbox.contentWindow, canvas); |
michael@0 | 719 | dt.setDragImage(canvas, -16 * scale, -16 * scale); |
michael@0 | 720 | |
michael@0 | 721 | event.stopPropagation(); |
michael@0 | 722 | ]]></handler> |
michael@0 | 723 | |
michael@0 | 724 | <handler event="dragend"><![CDATA[ |
michael@0 | 725 | let dt = event.dataTransfer; |
michael@0 | 726 | let draggedChat = dt.mozGetDataAt("application/x-moz-chatbox", 0); |
michael@0 | 727 | if (dt.mozUserCancelled || dt.dropEffect != "none") { |
michael@0 | 728 | return; |
michael@0 | 729 | } |
michael@0 | 730 | |
michael@0 | 731 | let eX = event.screenX; |
michael@0 | 732 | let eY = event.screenY; |
michael@0 | 733 | // screen.availLeft et. al. only check the screen that this window is on, |
michael@0 | 734 | // but we want to look at the screen the tab is being dropped onto. |
michael@0 | 735 | let sX = {}, sY = {}, sWidth = {}, sHeight = {}; |
michael@0 | 736 | Cc["@mozilla.org/gfx/screenmanager;1"] |
michael@0 | 737 | .getService(Ci.nsIScreenManager) |
michael@0 | 738 | .screenForRect(eX, eY, 1, 1) |
michael@0 | 739 | .GetAvailRect(sX, sY, sWidth, sHeight); |
michael@0 | 740 | // default size for the chat window as used in chatWindow.xul, use them |
michael@0 | 741 | // here to attempt to keep the window fully within the screen when |
michael@0 | 742 | // opening at the drop point. If the user has resized the window to |
michael@0 | 743 | // something larger (which gets persisted), at least a good portion of |
michael@0 | 744 | // the window should still be within the screen. |
michael@0 | 745 | let winWidth = 400; |
michael@0 | 746 | let winHeight = 420; |
michael@0 | 747 | // ensure new window entirely within screen |
michael@0 | 748 | let left = Math.min(Math.max(eX, sX.value), |
michael@0 | 749 | sX.value + sWidth.value - winWidth); |
michael@0 | 750 | let top = Math.min(Math.max(eY, sY.value), |
michael@0 | 751 | sY.value + sHeight.value - winHeight); |
michael@0 | 752 | |
michael@0 | 753 | let provider = Social._getProviderFromOrigin(draggedChat.content.getAttribute("origin")); |
michael@0 | 754 | this.detachChatbox(draggedChat, { screenX: left, screenY: top }, win => { |
michael@0 | 755 | win.document.title = provider.name; |
michael@0 | 756 | }); |
michael@0 | 757 | |
michael@0 | 758 | event.stopPropagation(); |
michael@0 | 759 | ]]></handler> |
michael@0 | 760 | </handlers> |
michael@0 | 761 | </binding> |
michael@0 | 762 | |
michael@0 | 763 | </bindings> |