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