mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-25 05:41:12 +00:00
3ad8dc0560
MozReview-Commit-ID: G9R8zd3WqG0 --HG-- extra : rebase_source : 46cfd345316c6491f490c9df25172b1a8a561571
914 lines
34 KiB
XML
914 lines
34 KiB
XML
<?xml version="1.0"?>
|
|
|
|
<bindings id="socialChatBindings"
|
|
xmlns="http://www.mozilla.org/xbl"
|
|
xmlns:xbl="http://www.mozilla.org/xbl"
|
|
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
|
|
|
|
<binding id="chatbox">
|
|
<content orient="vertical" mousethrough="never">
|
|
<xul:hbox class="chat-titlebar" xbl:inherits="minimized,selected,activity" align="baseline">
|
|
<xul:hbox flex="1" onclick="document.getBindingParent(this).onTitlebarClick(event);">
|
|
<xul:image class="chat-status-icon" xbl:inherits="src=image"/>
|
|
<xul:label class="chat-title" flex="1" xbl:inherits="crop=titlecrop,value=label" crop="end"/>
|
|
</xul:hbox>
|
|
<xul:toolbarbutton anonid="webRTC-shareScreen-icon"
|
|
class="notification-anchor-icon chat-toolbarbutton screen-icon"
|
|
oncommand="document.getBindingParent(this).showNotifications(this); event.stopPropagation();"/>
|
|
<xul:toolbarbutton anonid="webRTC-sharingScreen-icon"
|
|
class="notification-anchor-icon chat-toolbarbutton screen-icon in-use"
|
|
oncommand="document.getBindingParent(this).showNotifications(this); event.stopPropagation();"/>
|
|
<xul:toolbarbutton anonid="notification-icon" class="notification-anchor-icon chat-toolbarbutton"
|
|
oncommand="document.getBindingParent(this).showNotifications(this); event.stopPropagation();"/>
|
|
<xul:toolbarbutton anonid="minimize" class="chat-minimize-button chat-toolbarbutton"
|
|
oncommand="document.getBindingParent(this).toggle();"/>
|
|
<xul:toolbarbutton anonid="swap" class="chat-swap-button chat-toolbarbutton"
|
|
oncommand="document.getBindingParent(this).swapWindows();"/>
|
|
<xul:toolbarbutton anonid="close" class="chat-close-button chat-toolbarbutton"
|
|
oncommand="document.getBindingParent(this).close();"/>
|
|
</xul:hbox>
|
|
<xul:browser anonid="remote-content" class="chat-frame" flex="1"
|
|
context="contentAreaContextMenu"
|
|
disableglobalhistory="true"
|
|
frameType="social"
|
|
message="true"
|
|
messagemanagergroup="social"
|
|
tooltip="aHTMLTooltip"
|
|
remote="true"
|
|
xbl:inherits="src,origin"
|
|
type="content"/>
|
|
|
|
<xul:browser anonid="content" class="chat-frame" flex="1"
|
|
context="contentAreaContextMenu"
|
|
disableglobalhistory="true"
|
|
message="true"
|
|
messagemanagergroup="social"
|
|
tooltip="aHTMLTooltip"
|
|
xbl:inherits="src,origin"
|
|
type="content"/>
|
|
</content>
|
|
|
|
<implementation implements="nsIDOMEventListener, nsIMessageListener">
|
|
<constructor><![CDATA[
|
|
const kAnchorMap = new Map([
|
|
["", "notification-"],
|
|
["webRTC-shareScreen-", ""],
|
|
["webRTC-sharingScreen-", ""]
|
|
]);
|
|
const kBrowsers = [
|
|
document.getAnonymousElementByAttribute(this, "anonid", "content"),
|
|
document.getAnonymousElementByAttribute(this, "anonid", "remote-content")
|
|
];
|
|
for (let content of kBrowsers) {
|
|
for (let [getterPrefix, idPrefix] of kAnchorMap) {
|
|
let getter = getterPrefix + "popupnotificationanchor";
|
|
let anonid = (idPrefix || getterPrefix) + "icon";
|
|
content.__defineGetter__(getter, () => {
|
|
delete content[getter];
|
|
return content[getter] = document.getAnonymousElementByAttribute(
|
|
this, "anonid", anonid);
|
|
});
|
|
}
|
|
}
|
|
|
|
let mm = this.content.messageManager;
|
|
// process this._callbacks, then set to null so the chatbox creator
|
|
// knows to make new callbacks immediately.
|
|
if (this._callbacks) {
|
|
for (let callback of this._callbacks) {
|
|
callback(this);
|
|
}
|
|
this._callbacks = null;
|
|
}
|
|
|
|
mm.addMessageListener("Social:DOMTitleChanged", this);
|
|
|
|
mm.sendAsyncMessage("WaitForDOMContentLoaded");
|
|
mm.addMessageListener("DOMContentLoaded", function DOMContentLoaded(event) {
|
|
mm.removeMessageListener("DOMContentLoaded", DOMContentLoaded);
|
|
this.isActive = !this.minimized;
|
|
this._chat.loadButtonSet(this, this.getAttribute("buttonSet"));
|
|
this._deferredChatLoaded.resolve(this);
|
|
}.bind(this));
|
|
|
|
this.setActiveBrowser();
|
|
]]></constructor>
|
|
|
|
<field name="_deferredChatLoaded" readonly="true">
|
|
Promise.defer();
|
|
</field>
|
|
|
|
<property name="promiseChatLoaded">
|
|
<getter>
|
|
return this._deferredChatLoaded.promise;
|
|
</getter>
|
|
</property>
|
|
|
|
<property name="content">
|
|
<getter>
|
|
return document.getAnonymousElementByAttribute(this, "anonid",
|
|
(this.remote ? "remote-" : "") + "content");
|
|
</getter>
|
|
</property>
|
|
|
|
<field name="_chat" readonly="true">
|
|
Cu.import("resource:///modules/Chat.jsm", {}).Chat;
|
|
</field>
|
|
|
|
<property name="minimized">
|
|
<getter>
|
|
return this.getAttribute("minimized") == "true";
|
|
</getter>
|
|
<setter><![CDATA[
|
|
// Note that this.isActive is set via our transitionend handler so
|
|
// the content doesn't see intermediate values.
|
|
let parent = this.chatbar;
|
|
if (val) {
|
|
this.setAttribute("minimized", "true");
|
|
// If this chat is the selected one a new one needs to be selected.
|
|
if (parent && parent.selectedChat == this)
|
|
parent._selectAnotherChat();
|
|
} else {
|
|
this.removeAttribute("minimized");
|
|
// this chat gets selected.
|
|
if (parent)
|
|
parent.selectedChat = this;
|
|
}
|
|
]]></setter>
|
|
</property>
|
|
|
|
<property name="chatbar">
|
|
<getter>
|
|
if (this.parentNode.nodeName == "chatbar")
|
|
return this.parentNode;
|
|
return null;
|
|
</getter>
|
|
</property>
|
|
|
|
<property name="isActive">
|
|
<getter>
|
|
return this.content.docShellIsActive;
|
|
</getter>
|
|
<setter>
|
|
this.content.docShellIsActive = !!val;
|
|
|
|
// Bug 1256431 to remove socialFrameShow/Hide from hello, keep this
|
|
// until that is complete.
|
|
// let the chat frame know if it is being shown or hidden
|
|
this.content.messageManager.sendAsyncMessage("Social:CustomEvent", {
|
|
name: val ? "socialFrameShow" : "socialFrameHide"
|
|
});
|
|
</setter>
|
|
</property>
|
|
|
|
<field name="_remote">false</field>
|
|
<property name="remote" onget="return this._remote;">
|
|
<setter><![CDATA[
|
|
this._remote = !!val;
|
|
|
|
this.setActiveBrowser();
|
|
]]></setter>
|
|
</property>
|
|
|
|
<method name="setActiveBrowser">
|
|
<body><![CDATA[
|
|
// Make sure we only show one browser element at a time.
|
|
let content = document.getAnonymousElementByAttribute(this, "anonid", "content");
|
|
let remoteContent = document.getAnonymousElementByAttribute(this, "anonid", "remote-content");
|
|
remoteContent.setAttribute("hidden", !this.remote);
|
|
content.setAttribute("hidden", this.remote);
|
|
remoteContent.removeAttribute("src");
|
|
content.removeAttribute("src");
|
|
|
|
if (this.src) {
|
|
this.setAttribute("src", this.src);
|
|
|
|
// Stop loading of the document - that is set before this method was
|
|
// called - in the now hidden browser.
|
|
(this.remote ? content : remoteContent).setAttribute("src", "about:blank");
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="showNotifications">
|
|
<parameter name="aAnchor"/>
|
|
<body><![CDATA[
|
|
PopupNotifications._reshowNotifications(aAnchor,
|
|
this.content);
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="swapDocShells">
|
|
<parameter name="aTarget"/>
|
|
<body><![CDATA[
|
|
aTarget.setAttribute("label", this.content.contentTitle);
|
|
|
|
aTarget.remote = this.remote;
|
|
aTarget.src = this.src;
|
|
let content = aTarget.content;
|
|
content.setAttribute("origin", this.content.getAttribute("origin"));
|
|
content.popupnotificationanchor.className = this.content.popupnotificationanchor.className;
|
|
content.swapDocShells(this.content);
|
|
|
|
// When a chat window is attached or detached, the docShell hosting
|
|
// the chat document is swapped to the newly created chat window.
|
|
// (Be it inside a popup or back inside a chatbox element attached to
|
|
// the chatbar.)
|
|
// Since a swapDocShells call does not swap the messageManager instances
|
|
// attached to a browser, we'll need to add the message listeners to
|
|
// the new messageManager. This is not a bug in swapDocShells, merely
|
|
// a design decision.
|
|
content.messageManager.addMessageListener("Social:DOMTitleChanged", content);
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="setDecorationAttributes">
|
|
<parameter name="aTarget"/>
|
|
<body><![CDATA[
|
|
if (this.hasAttribute("customSize"))
|
|
aTarget.setAttribute("customSize", this.getAttribute("customSize"));
|
|
this._chat.loadButtonSet(aTarget, this.getAttribute("buttonSet"));
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="onTitlebarClick">
|
|
<parameter name="aEvent"/>
|
|
<body><![CDATA[
|
|
if (!this.chatbar)
|
|
return;
|
|
if (aEvent.button == 0) { // left-click: toggle minimized.
|
|
this.toggle();
|
|
// if we restored it, we want to focus it.
|
|
if (!this.minimized)
|
|
this.chatbar.focus();
|
|
} else if (aEvent.button == 1) // middle-click: close chat
|
|
this.close();
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="close">
|
|
<body><![CDATA[
|
|
if (this.chatbar)
|
|
this.chatbar.remove(this);
|
|
else
|
|
window.close();
|
|
|
|
if (!this.swappingWindows)
|
|
this.dispatchEvent(new CustomEvent("ChatboxClosed"));
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="swapWindows">
|
|
<body><![CDATA[
|
|
let deferred = Promise.defer();
|
|
let title = this.getAttribute("label");
|
|
if (this.chatbar) {
|
|
this.chatbar.detachChatbox(this, { "centerscreen": "yes" }).then(
|
|
chatbox => {
|
|
chatbox.content.messageManager.sendAsyncMessage("Social:SetDocumentTitle", {
|
|
title: title
|
|
});
|
|
deferred.resolve(chatbox);
|
|
}
|
|
);
|
|
} else {
|
|
// attach this chatbox to the topmost browser window
|
|
let Chat = Cu.import("resource:///modules/Chat.jsm").Chat;
|
|
let win = Chat.findChromeWindowForChats();
|
|
let chatbar = win.document.getElementById("pinnedchats");
|
|
let origin = this.content.getAttribute("origin");
|
|
let cb = chatbar.openChat({
|
|
origin: origin,
|
|
title: title,
|
|
url: "about:blank"
|
|
});
|
|
|
|
cb.promiseChatLoaded.then(
|
|
() => {
|
|
this.setDecorationAttributes(cb);
|
|
|
|
this.swapDocShells(cb);
|
|
|
|
chatbar.focus();
|
|
this.swappingWindows = true;
|
|
this.close();
|
|
|
|
// chatboxForURL is a map of URL -> chatbox used to avoid opening
|
|
// duplicate chat windows. Ensure reattached chat windows aren't
|
|
// registered with about:blank as their URL, otherwise reattaching
|
|
// more than one chat window isn't possible.
|
|
chatbar.chatboxForURL.delete("about:blank");
|
|
chatbar.chatboxForURL.set(this.src, Cu.getWeakReference(cb));
|
|
|
|
cb.content.messageManager.sendAsyncMessage("Social:CustomEvent", {
|
|
name: "socialFrameAttached"
|
|
});
|
|
|
|
deferred.resolve(cb);
|
|
}
|
|
);
|
|
}
|
|
return deferred.promise;
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="toggle">
|
|
<body><![CDATA[
|
|
this.minimized = !this.minimized;
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="setTitle">
|
|
<body><![CDATA[
|
|
try {
|
|
this.setAttribute("label", this.content.contentTitle);
|
|
} catch (ex) {}
|
|
if (this.chatbar)
|
|
this.chatbar.updateTitlebar(this);
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="receiveMessage">
|
|
<parameter name="aMessage" />
|
|
<body><![CDATA[
|
|
switch (aMessage.name) {
|
|
case "Social:DOMTitleChanged":
|
|
this.setTitle();
|
|
break;
|
|
}
|
|
]]></body>
|
|
</method>
|
|
</implementation>
|
|
|
|
<handlers>
|
|
<handler event="focus" phase="capturing">
|
|
if (this.chatbar)
|
|
this.chatbar.selectedChat = this;
|
|
</handler>
|
|
<handler event="DOMTitleChanged">
|
|
this.setTitle();
|
|
</handler>
|
|
<handler event="DOMLinkAdded"><![CDATA[
|
|
// Much of this logic is from DOMLinkHandler in browser.js.
|
|
// This sets the presence icon for a chat user, we simply use favicon
|
|
// style updating.
|
|
let link = event.originalTarget;
|
|
let rel = link.rel && link.rel.toLowerCase();
|
|
if (!link || !link.ownerDocument || !rel || !link.href)
|
|
return;
|
|
if (link.rel.indexOf("icon") < 0)
|
|
return;
|
|
|
|
let ContentLinkHandler = Cu.import("resource:///modules/ContentLinkHandler.jsm", {})
|
|
.ContentLinkHandler;
|
|
let uri = ContentLinkHandler.getLinkIconURI(link);
|
|
if (!uri)
|
|
return;
|
|
|
|
// We made it this far, use it.
|
|
this.setAttribute("image", uri.spec);
|
|
if (this.chatbar)
|
|
this.chatbar.updateTitlebar(this);
|
|
]]></handler>
|
|
<handler event="transitionend">
|
|
if (this.isActive == this.minimized)
|
|
this.isActive = !this.minimized;
|
|
</handler>
|
|
</handlers>
|
|
</binding>
|
|
|
|
<binding id="chatbar">
|
|
<content>
|
|
<xul:hbox align="end" pack="end" anonid="innerbox" class="chatbar-innerbox" mousethrough="always" flex="1">
|
|
<xul:spacer flex="1" anonid="spacer" class="chatbar-overflow-spacer"/>
|
|
<xul:toolbarbutton anonid="nub" class="chatbar-button" type="menu" collapsed="true" mousethrough="never">
|
|
<xul:menupopup anonid="nubMenu" oncommand="document.getBindingParent(this).showChat(event.target.chat)"/>
|
|
</xul:toolbarbutton>
|
|
<children/>
|
|
</xul:hbox>
|
|
</content>
|
|
|
|
<implementation implements="nsIDOMEventListener">
|
|
<constructor>
|
|
// to avoid reflows we cache the width of the nub.
|
|
this.cachedWidthNub = 0;
|
|
this._selectedChat = null;
|
|
</constructor>
|
|
|
|
<field name="innerbox" readonly="true">
|
|
document.getAnonymousElementByAttribute(this, "anonid", "innerbox");
|
|
</field>
|
|
|
|
<field name="menupopup" readonly="true">
|
|
document.getAnonymousElementByAttribute(this, "anonid", "nubMenu");
|
|
</field>
|
|
|
|
<field name="nub" readonly="true">
|
|
document.getAnonymousElementByAttribute(this, "anonid", "nub");
|
|
</field>
|
|
|
|
<method name="focus">
|
|
<body><![CDATA[
|
|
if (!this.selectedChat)
|
|
return;
|
|
this.selectedChat.content.messageManager.sendAsyncMessage("Social:EnsureFocus");
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="_isChatFocused">
|
|
<parameter name="aChatbox"/>
|
|
<body><![CDATA[
|
|
// If there are no XBL bindings for the chat it can't be focused.
|
|
if (!aChatbox.content)
|
|
return false;
|
|
let fw = Services.focus.focusedWindow;
|
|
if (!fw)
|
|
return false;
|
|
// We want to see if the focused window is in the subtree below our browser...
|
|
let containingBrowser = fw.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIWebNavigation)
|
|
.QueryInterface(Ci.nsIDocShell)
|
|
.chromeEventHandler;
|
|
return containingBrowser == aChatbox.content;
|
|
]]></body>
|
|
</method>
|
|
|
|
<property name="selectedChat">
|
|
<getter><![CDATA[
|
|
return this._selectedChat;
|
|
]]></getter>
|
|
<setter><![CDATA[
|
|
// this is pretty horrible, but we:
|
|
// * want to avoid doing touching 'selected' attribute when the
|
|
// specified chat is already selected.
|
|
// * remove 'activity' attribute on newly selected tab *even if*
|
|
// newly selected is already selected.
|
|
// * need to handle either current or new being null.
|
|
if (this._selectedChat != val) {
|
|
if (this._selectedChat) {
|
|
this._selectedChat.removeAttribute("selected");
|
|
}
|
|
this._selectedChat = val;
|
|
if (val) {
|
|
this._selectedChat.setAttribute("selected", "true");
|
|
}
|
|
}
|
|
if (val) {
|
|
this._selectedChat.removeAttribute("activity");
|
|
}
|
|
]]></setter>
|
|
</property>
|
|
|
|
<field name="menuitemMap">new WeakMap()</field>
|
|
<field name="chatboxForURL">new Map();</field>
|
|
|
|
<property name="hasCollapsedChildren">
|
|
<getter><![CDATA[
|
|
return !!this.querySelector("[collapsed]");
|
|
]]></getter>
|
|
</property>
|
|
|
|
<property name="collapsedChildren">
|
|
<getter><![CDATA[
|
|
// A generator yielding all collapsed chatboxes, in the order in
|
|
// which they should be restored.
|
|
return function*() {
|
|
let child = this.lastElementChild;
|
|
while (child) {
|
|
if (child.collapsed)
|
|
yield child;
|
|
child = child.previousElementSibling;
|
|
}
|
|
}
|
|
]]></getter>
|
|
</property>
|
|
|
|
<property name="visibleChildren">
|
|
<getter><![CDATA[
|
|
// A generator yielding all non-collapsed chatboxes.
|
|
return function*() {
|
|
let child = this.firstElementChild;
|
|
while (child) {
|
|
if (!child.collapsed)
|
|
yield child;
|
|
child = child.nextElementSibling;
|
|
}
|
|
}
|
|
]]></getter>
|
|
</property>
|
|
|
|
<property name="collapsibleChildren">
|
|
<getter><![CDATA[
|
|
// A generator yielding all children which are able to be collapsed
|
|
// in the order in which they should be collapsed.
|
|
// (currently this is all visible ones other than the selected one.)
|
|
return function*() {
|
|
for (let child of this.visibleChildren())
|
|
if (child != this.selectedChat)
|
|
yield child;
|
|
}
|
|
]]></getter>
|
|
</property>
|
|
|
|
<method name="_selectAnotherChat">
|
|
<body><![CDATA[
|
|
// Select a different chat (as the currently selected one is no
|
|
// longer suitable as the selection - maybe it is being minimized or
|
|
// closed.) We only select non-minimized and non-collapsed chats,
|
|
// and if none are found, set the selectedChat to null.
|
|
// It's possible in the future we will track most-recently-selected
|
|
// chats or similar to find the "best" candidate - for now though
|
|
// the choice is somewhat arbitrary.
|
|
let moveFocus = this.selectedChat && this._isChatFocused(this.selectedChat);
|
|
for (let other of this.children) {
|
|
if (other != this.selectedChat && !other.minimized && !other.collapsed) {
|
|
this.selectedChat = other;
|
|
if (moveFocus)
|
|
this.focus();
|
|
return;
|
|
}
|
|
}
|
|
// can't find another - so set no chat as selected.
|
|
this.selectedChat = null;
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="updateTitlebar">
|
|
<parameter name="aChatbox"/>
|
|
<body><![CDATA[
|
|
if (aChatbox.collapsed) {
|
|
let menuitem = this.menuitemMap.get(aChatbox);
|
|
if (aChatbox.getAttribute("activity")) {
|
|
menuitem.setAttribute("activity", true);
|
|
this.nub.setAttribute("activity", true);
|
|
}
|
|
menuitem.setAttribute("label", aChatbox.getAttribute("label"));
|
|
menuitem.setAttribute("image", aChatbox.getAttribute("image"));
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="calcTotalWidthOf">
|
|
<parameter name="aElement"/>
|
|
<body><![CDATA[
|
|
let cs = document.defaultView.getComputedStyle(aElement);
|
|
let margins = parseInt(cs.marginLeft) + parseInt(cs.marginRight);
|
|
return aElement.getBoundingClientRect().width + margins;
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="getTotalChildWidth">
|
|
<parameter name="aChatbox"/>
|
|
<body><![CDATA[
|
|
// These are from the CSS for the chatbox and must be kept in sync.
|
|
// We can't use calcTotalWidthOf due to the transitions...
|
|
const CHAT_WIDTH_OPEN = 300;
|
|
const CHAT_WIDTH_OPEN_ALT = 350;
|
|
const CHAT_WIDTH_MINIMIZED = 160;
|
|
let openWidth = aChatbox.hasAttribute("customSize") ?
|
|
CHAT_WIDTH_OPEN_ALT : CHAT_WIDTH_OPEN;
|
|
|
|
return aChatbox.minimized ? CHAT_WIDTH_MINIMIZED : openWidth;
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="collapseChat">
|
|
<parameter name="aChatbox"/>
|
|
<body><![CDATA[
|
|
// we ensure that the cached width for a child of this type is
|
|
// up-to-date so we can use it when resizing.
|
|
this.getTotalChildWidth(aChatbox);
|
|
aChatbox.collapsed = true;
|
|
aChatbox.isActive = false;
|
|
let menu = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "menuitem");
|
|
menu.setAttribute("class", "menuitem-iconic");
|
|
menu.setAttribute("label", aChatbox.content.contentTitle);
|
|
menu.setAttribute("image", aChatbox.getAttribute("image"));
|
|
menu.chat = aChatbox;
|
|
this.menuitemMap.set(aChatbox, menu);
|
|
this.menupopup.appendChild(menu);
|
|
this.nub.collapsed = false;
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="showChat">
|
|
<parameter name="aChatbox"/>
|
|
<parameter name="aMode"/>
|
|
<body><![CDATA[
|
|
if ((aMode != "minimized") && aChatbox.minimized)
|
|
aChatbox.minimized = false;
|
|
if (this.selectedChat != aChatbox)
|
|
this.selectedChat = aChatbox;
|
|
if (!aChatbox.collapsed)
|
|
return; // already showing - no more to do.
|
|
this._showChat(aChatbox);
|
|
// showing a collapsed chat might mean another needs to be collapsed
|
|
// to make room...
|
|
this.resize();
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="_showChat">
|
|
<parameter name="aChatbox"/>
|
|
<body><![CDATA[
|
|
// the actual implementation - doesn't check for overflow, assumes
|
|
// collapsed, etc.
|
|
let menuitem = this.menuitemMap.get(aChatbox);
|
|
this.menuitemMap.delete(aChatbox);
|
|
this.menupopup.removeChild(menuitem);
|
|
aChatbox.collapsed = false;
|
|
aChatbox.isActive = !aChatbox.minimized;
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="remove">
|
|
<parameter name="aChatbox"/>
|
|
<body><![CDATA[
|
|
this._remove(aChatbox);
|
|
// The removal of a chat may mean a collapsed one can spring up,
|
|
// or that the popup should be hidden. We also defer the selection
|
|
// of another chat until after a resize, as a new candidate may
|
|
// become uncollapsed after the resize.
|
|
this.resize();
|
|
if (this.selectedChat == aChatbox) {
|
|
this._selectAnotherChat();
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="_remove">
|
|
<parameter name="aChatbox"/>
|
|
<body><![CDATA[
|
|
this.removeChild(aChatbox);
|
|
// child might have been collapsed.
|
|
let menuitem = this.menuitemMap.get(aChatbox);
|
|
if (menuitem) {
|
|
this.menuitemMap.delete(aChatbox);
|
|
this.menupopup.removeChild(menuitem);
|
|
}
|
|
this.chatboxForURL.delete(aChatbox.src);
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="openChat">
|
|
<parameter name="aOptions"/>
|
|
<parameter name="aCallback"/>
|
|
<body><![CDATA[
|
|
let {origin, title, url, mode} = aOptions;
|
|
let cb = this.chatboxForURL.get(url);
|
|
if (cb && (cb = cb.get())) {
|
|
// A chatbox is still alive to us when it's parented and still has
|
|
// content.
|
|
if (cb.parentNode) {
|
|
this.showChat(cb, mode);
|
|
if (aCallback) {
|
|
if (cb._callbacks == null) {
|
|
// Chatbox has already been created, so callback now.
|
|
aCallback(cb);
|
|
} else {
|
|
// Chatbox is yet to have bindings created...
|
|
cb._callbacks.push(aCallback);
|
|
}
|
|
}
|
|
return cb;
|
|
}
|
|
this.chatboxForURL.delete(url);
|
|
}
|
|
cb = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "chatbox");
|
|
cb._callbacks = [];
|
|
if (aCallback) {
|
|
// _callbacks is a javascript property instead of a <field> as it
|
|
// must exist before the (possibly delayed) bindings are created.
|
|
cb._callbacks.push(aCallback);
|
|
}
|
|
|
|
cb.remote = !!aOptions.remote;
|
|
// src also a javascript property; the src attribute is set in the ctor.
|
|
cb.src = url;
|
|
if (mode == "minimized")
|
|
cb.setAttribute("minimized", "true");
|
|
cb.setAttribute("origin", origin);
|
|
cb.setAttribute("label", title);
|
|
this.insertBefore(cb, this.firstChild);
|
|
this.selectedChat = cb;
|
|
this.chatboxForURL.set(url, Cu.getWeakReference(cb));
|
|
this.resize();
|
|
return cb;
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="resize">
|
|
<body><![CDATA[
|
|
// Checks the current size against the collapsed state of children
|
|
// and collapses or expands as necessary such that as many as possible
|
|
// are shown.
|
|
// So 2 basic strategies:
|
|
// * Collapse/Expand one at a time until we can't collapse/expand any
|
|
// more - but this is one reflow per change.
|
|
// * Calculate the dimensions ourself and choose how many to collapse
|
|
// or expand based on this, then do them all in one go. This is one
|
|
// reflow regardless of how many we change.
|
|
// So we go the more complicated but more efficient second option...
|
|
let availWidth = this.getBoundingClientRect().width;
|
|
let currentWidth = 0;
|
|
if (!this.nub.collapsed) { // the nub is visible.
|
|
if (!this.cachedWidthNub)
|
|
this.cachedWidthNub = this.calcTotalWidthOf(this.nub);
|
|
currentWidth += this.cachedWidthNub;
|
|
}
|
|
for (let child of this.visibleChildren()) {
|
|
currentWidth += this.getTotalChildWidth(child);
|
|
}
|
|
|
|
if (currentWidth > availWidth) {
|
|
// we need to collapse some.
|
|
let toCollapse = [];
|
|
for (let child of this.collapsibleChildren()) {
|
|
if (currentWidth <= availWidth)
|
|
break;
|
|
toCollapse.push(child);
|
|
currentWidth -= this.getTotalChildWidth(child);
|
|
}
|
|
if (toCollapse.length) {
|
|
for (let child of toCollapse)
|
|
this.collapseChat(child);
|
|
}
|
|
} else if (currentWidth < availWidth) {
|
|
// we *might* be able to expand some - see how many.
|
|
// XXX - if this was clever, it could know when removing the nub
|
|
// leaves enough space to show all collapsed
|
|
let toShow = [];
|
|
for (let child of this.collapsedChildren()) {
|
|
currentWidth += this.getTotalChildWidth(child);
|
|
if (currentWidth > availWidth)
|
|
break;
|
|
toShow.push(child);
|
|
}
|
|
for (let child of toShow)
|
|
this._showChat(child);
|
|
|
|
// If none remain collapsed remove the nub.
|
|
if (!this.hasCollapsedChildren) {
|
|
this.nub.collapsed = true;
|
|
}
|
|
}
|
|
// else: achievement unlocked - we are pixel-perfect!
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="handleEvent">
|
|
<parameter name="aEvent"/>
|
|
<body><![CDATA[
|
|
if (aEvent.type == "resize") {
|
|
this.resize();
|
|
}
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="_getDragTarget">
|
|
<parameter name="event"/>
|
|
<body><![CDATA[
|
|
return event.target.localName == "chatbox" ? event.target : null;
|
|
]]></body>
|
|
</method>
|
|
|
|
<!-- Moves a chatbox to a new window. Returns a promise that is resolved
|
|
once the move to the other window is complete.
|
|
-->
|
|
<method name="detachChatbox">
|
|
<parameter name="aChatbox"/>
|
|
<parameter name="aOptions"/>
|
|
<body><![CDATA[
|
|
let deferred = Promise.defer();
|
|
let chatbar = this;
|
|
let options = "";
|
|
for (let name in aOptions)
|
|
options += "," + name + "=" + aOptions[name];
|
|
|
|
let otherWin = window.openDialog("chrome://browser/content/chatWindow.xul",
|
|
"_blank", "chrome,all,dialog=no" + options);
|
|
|
|
otherWin.addEventListener("load", function _chatLoad(event) {
|
|
if (event.target != otherWin.document)
|
|
return;
|
|
|
|
if (aChatbox.hasAttribute("customSize")) {
|
|
otherWin.document.getElementById("chat-window").
|
|
setAttribute("customSize", aChatbox.getAttribute("customSize"));
|
|
}
|
|
|
|
otherWin.removeEventListener("load", _chatLoad, true);
|
|
let otherChatbox = otherWin.document.getElementById("chatter");
|
|
aChatbox.setDecorationAttributes(otherChatbox);
|
|
aChatbox.swapDocShells(otherChatbox);
|
|
|
|
aChatbox.swappingWindows = true;
|
|
aChatbox.close();
|
|
let url = aChatbox.src;
|
|
chatbar.chatboxForURL.set(url, Cu.getWeakReference(otherChatbox));
|
|
|
|
// All processing is done, now we can fire the event.
|
|
otherChatbox.content.messageManager.sendAsyncMessage("Social:CustomEvent", {
|
|
name: "socialFrameDetached"
|
|
});
|
|
|
|
Services.obs.addObserver(function onDOMWindowClosed(subject) {
|
|
if (subject !== otherWin)
|
|
return;
|
|
|
|
Services.obs.removeObserver(onDOMWindowClosed, "domwindowclosed");
|
|
chatbar.chatboxForURL.delete(url);
|
|
|
|
if (!otherChatbox.swappingWindows)
|
|
otherChatbox.dispatchEvent(new CustomEvent("ChatboxClosed"));
|
|
}, "domwindowclosed", false);
|
|
|
|
deferred.resolve(otherChatbox);
|
|
}, true);
|
|
return deferred.promise;
|
|
]]></body>
|
|
</method>
|
|
|
|
</implementation>
|
|
|
|
<handlers>
|
|
<handler event="popupshown"><![CDATA[
|
|
this.nub.removeAttribute("activity");
|
|
]]></handler>
|
|
<handler event="load"><![CDATA[
|
|
window.addEventListener("resize", this, true);
|
|
]]></handler>
|
|
<handler event="unload"><![CDATA[
|
|
window.removeEventListener("resize", this, true);
|
|
]]></handler>
|
|
|
|
<handler event="dragstart"><![CDATA[
|
|
// chat window dragging is essentially duplicated from tabbrowser.xml
|
|
// to acheive the same visual experience
|
|
let chatbox = this._getDragTarget(event);
|
|
if (!chatbox) {
|
|
return;
|
|
}
|
|
|
|
let dt = event.dataTransfer;
|
|
// we do not set a url in the drag data to prevent moving to tabbrowser
|
|
// or otherwise having unexpected drop handlers do something with our
|
|
// chatbox
|
|
dt.mozSetDataAt("application/x-moz-chatbox", chatbox, 0);
|
|
|
|
// Set the cursor to an arrow during tab drags.
|
|
dt.mozCursor = "default";
|
|
|
|
// Create a canvas to which we capture the current tab.
|
|
// Until canvas is HiDPI-aware (bug 780362), we need to scale the desired
|
|
// canvas size (in CSS pixels) to the window's backing resolution in order
|
|
// to get a full-resolution drag image for use on HiDPI displays.
|
|
let windowUtils = window.getInterface(Ci.nsIDOMWindowUtils);
|
|
let scale = windowUtils.screenPixelsPerCSSPixel / windowUtils.fullZoom;
|
|
let canvas = document.createElementNS("http://www.w3.org/1999/xhtml", "canvas");
|
|
canvas.mozOpaque = true;
|
|
canvas.width = 160 * scale;
|
|
canvas.height = 90 * scale;
|
|
PageThumbs.captureToCanvas(chatbox, canvas);
|
|
dt.setDragImage(canvas, -16 * scale, -16 * scale);
|
|
|
|
event.stopPropagation();
|
|
]]></handler>
|
|
|
|
<handler event="dragend"><![CDATA[
|
|
let dt = event.dataTransfer;
|
|
let draggedChat = dt.mozGetDataAt("application/x-moz-chatbox", 0);
|
|
if (dt.mozUserCancelled || dt.dropEffect != "none") {
|
|
return;
|
|
}
|
|
|
|
let eX = event.screenX;
|
|
let eY = event.screenY;
|
|
// screen.availLeft et. al. only check the screen that this window is on,
|
|
// but we want to look at the screen the tab is being dropped onto.
|
|
let sX = {}, sY = {}, sWidth = {}, sHeight = {};
|
|
Cc["@mozilla.org/gfx/screenmanager;1"]
|
|
.getService(Ci.nsIScreenManager)
|
|
.screenForRect(eX, eY, 1, 1)
|
|
.GetAvailRect(sX, sY, sWidth, sHeight);
|
|
// default size for the chat window as used in chatWindow.xul, use them
|
|
// here to attempt to keep the window fully within the screen when
|
|
// opening at the drop point. If the user has resized the window to
|
|
// something larger (which gets persisted), at least a good portion of
|
|
// the window should still be within the screen.
|
|
let winWidth = 400;
|
|
let winHeight = 420;
|
|
// ensure new window entirely within screen
|
|
let left = Math.min(Math.max(eX, sX.value),
|
|
sX.value + sWidth.value - winWidth);
|
|
let top = Math.min(Math.max(eY, sY.value),
|
|
sY.value + sHeight.value - winHeight);
|
|
|
|
this.detachChatbox(draggedChat, { screenX: left, screenY: top });
|
|
event.stopPropagation();
|
|
]]></handler>
|
|
</handlers>
|
|
</binding>
|
|
|
|
</bindings>
|