mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
564 lines
21 KiB
XML
564 lines
21 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"
|
|
onclick="document.getBindingParent(this).onTitlebarClick(event);" align="baseline">
|
|
<xul:image class="chat-status-icon" xbl:inherits="src=image"/>
|
|
<xul:label class="chat-title" flex="1" xbl:inherits="value=label" crop="center"/>
|
|
<xul:toolbarbutton class="chat-close-button chat-toolbarbutton"
|
|
oncommand="document.getBindingParent(this).close();"/>
|
|
</xul:hbox>
|
|
<xul:iframe anonid="iframe" class="chat-frame" flex="1"
|
|
context="contentAreaContextMenu"
|
|
tooltip="aHTMLTooltip"
|
|
xbl:inherits="src,origin,collapsed=minimized" type="content"/>
|
|
</content>
|
|
|
|
<implementation implements="nsIDOMEventListener">
|
|
<constructor><![CDATA[
|
|
let Social = Components.utils.import("resource:///modules/Social.jsm", {}).Social;
|
|
Social.setErrorListener(this.iframe, function(iframe) {
|
|
iframe.webNavigation.loadURI("about:socialerror?mode=compactInfo", null, null, null, null);
|
|
});
|
|
let iframeWindow = this.iframe.contentWindow;
|
|
this.addEventListener("DOMContentLoaded", function DOMContentLoaded(event) {
|
|
if (event.target != this.iframe.contentDocument)
|
|
return;
|
|
this.removeEventListener("DOMContentLoaded", DOMContentLoaded, true);
|
|
this.isActive = !this.minimized;
|
|
// process this._callbacks, then set to null so the chatbox creator
|
|
// knows to make new callbacks immediately.
|
|
for (let callback of this._callbacks) {
|
|
if (callback)
|
|
callback(iframeWindow);
|
|
}
|
|
this._callbacks = null;
|
|
|
|
// content can send a socialChatActivity event to have the UI update.
|
|
let chatActivity = function() {
|
|
this.setAttribute("activity", true);
|
|
this.parentNode.updateTitlebar(this);
|
|
}.bind(this);
|
|
iframeWindow.addEventListener("socialChatActivity", chatActivity);
|
|
iframeWindow.addEventListener("unload", function unload() {
|
|
iframeWindow.removeEventListener("unload", unload);
|
|
iframeWindow.removeEventListener("socialChatActivity", chatActivity);
|
|
});
|
|
}, true);
|
|
this.setAttribute("src", this.src);
|
|
]]></constructor>
|
|
|
|
<field name="iframe" readonly="true">
|
|
document.getAnonymousElementByAttribute(this, "anonid", "iframe");
|
|
</field>
|
|
|
|
<property name="minimized">
|
|
<getter>
|
|
return this.getAttribute("minimized") == "true";
|
|
</getter>
|
|
<setter><![CDATA[
|
|
this.isActive = !val;
|
|
let parent = this.parentNode;
|
|
if (val) {
|
|
this.setAttribute("minimized", "true");
|
|
// If this chat is the selected one a new one needs to be selected.
|
|
if (parent.selectedChat == this)
|
|
parent._selectAnotherChat();
|
|
} else {
|
|
this.removeAttribute("minimized");
|
|
// this chat gets selected.
|
|
parent.selectedChat = this;
|
|
}
|
|
]]></setter>
|
|
</property>
|
|
|
|
<property name="isActive">
|
|
<getter>
|
|
return this.iframe.docShell.isActive;
|
|
</getter>
|
|
<setter>
|
|
this.iframe.docShell.isActive = !!val;
|
|
|
|
// let the chat frame know if it is being shown or hidden
|
|
let evt = this.iframe.contentDocument.createEvent("CustomEvent");
|
|
evt.initCustomEvent(val ? "socialFrameShow" : "socialFrameHide", true, true, {});
|
|
this.iframe.contentDocument.documentElement.dispatchEvent(evt);
|
|
</setter>
|
|
</property>
|
|
|
|
<method name="onTitlebarClick">
|
|
<parameter name="aEvent"/>
|
|
<body><![CDATA[
|
|
if (aEvent.button == 0) { // left-click: toggle minimized.
|
|
this.toggle();
|
|
// if we restored it, we want to focus it.
|
|
if (!this.minimized)
|
|
this.parentNode.focus();
|
|
} else if (aEvent.button == 1) // middle-click: close chat
|
|
this.close();
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="close">
|
|
<body><![CDATA[
|
|
this.parentNode.remove(this);
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="toggle">
|
|
<body><![CDATA[
|
|
this.minimized = !this.minimized;
|
|
]]></body>
|
|
</method>
|
|
</implementation>
|
|
|
|
<handlers>
|
|
<handler event="focus">
|
|
this.parentNode.selectedChat = this;
|
|
</handler>
|
|
<handler event="DOMTitleChanged"><![CDATA[
|
|
this.setAttribute('label', this.iframe.contentDocument.title);
|
|
this.parentNode.updateTitlebar(this);
|
|
]]></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 uri = DOMLinkHandler.getLinkIconURI(link);
|
|
if (!uri)
|
|
return;
|
|
|
|
// we made it this far, use it
|
|
this.setAttribute('image', uri.spec);
|
|
this.parentNode.updateTitlebar(this);
|
|
]]></handler>
|
|
</handlers>
|
|
</binding>
|
|
|
|
<binding id="chatbar">
|
|
<content>
|
|
<xul:hbox align="end" pack="end" anonid="innerbox" class="chatbar-innerbox" mousethrough="always" flex="1">
|
|
<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>
|
|
<xul:spacer flex="1" anonid="spacer" class="chatbar-overflow-spacer"/>
|
|
<children/>
|
|
</xul:hbox>
|
|
</content>
|
|
|
|
<implementation implements="nsIDOMEventListener">
|
|
<constructor>
|
|
// to avoid reflows we cache the values for various widths.
|
|
this.cachedWidthOpen = 0;
|
|
this.cachedWidthMinimized = 0;
|
|
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;
|
|
Services.focus.focusedWindow = this.selectedChat.iframe.contentWindow;
|
|
]]></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.iframe)
|
|
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 iframe...
|
|
let containingBrowser = fw.QueryInterface(Ci.nsIInterfaceRequestor)
|
|
.getInterface(Ci.nsIWebNavigation)
|
|
.QueryInterface(Ci.nsIDocShell)
|
|
.chromeEventHandler;
|
|
return containingBrowser == aChatbox.iframe;
|
|
]]></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.
|
|
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.
|
|
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.)
|
|
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[
|
|
// gets the width of a child, using/setting the cached value for
|
|
// children of this type.
|
|
// DOES NOT take collapsed into account - ie, this is the width
|
|
// of a child assuming it is *not* collapsed. (collapsed chats
|
|
// have a width of zero as they are not shown).
|
|
if (aChatbox.minimized) {
|
|
if (!this.cachedWidthMinimized) {
|
|
if (aChatbox.collapsed)
|
|
throw new Error("can't calculate size of collapsed chat!");
|
|
this.cachedWidthMinimized = this.calcTotalWidthOf(aChatbox);
|
|
}
|
|
return this.cachedWidthMinimized;
|
|
}
|
|
if (!this.cachedWidthOpen) {
|
|
if (aChatbox.collapsed)
|
|
throw new Error("can't calculate size of collapsed chat!");
|
|
this.cachedWidthOpen = this.calcTotalWidthOf(aChatbox);
|
|
}
|
|
return this.cachedWidthOpen;
|
|
]]></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.iframe.contentDocument.title);
|
|
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[
|
|
aChatbox.iframe.socialErrorListener.remove();
|
|
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.getAttribute('src'));
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="removeAll">
|
|
<body><![CDATA[
|
|
this.selectedChat = null;
|
|
while (this.firstElementChild) {
|
|
this._remove(this.firstElementChild);
|
|
}
|
|
// and the nub/popup must also die.
|
|
this.nub.collapsed = true;
|
|
]]></body>
|
|
</method>
|
|
|
|
<method name="openChat">
|
|
<parameter name="aProvider"/>
|
|
<parameter name="aURL"/>
|
|
<parameter name="aCallback"/>
|
|
<parameter name="aMode"/>
|
|
<body><![CDATA[
|
|
let cb = this.chatboxForURL.get(aURL);
|
|
if (cb) {
|
|
cb = cb.get();
|
|
if (cb.parentNode) {
|
|
this.showChat(cb, aMode);
|
|
if (aCallback) {
|
|
if (cb._callbacks == null) {
|
|
// DOMContentLoaded has already fired, so callback now.
|
|
aCallback(cb.iframe.contentWindow);
|
|
} else {
|
|
// DOMContentLoaded for this chat is yet to fire...
|
|
cb._callbacks.push(aCallback);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
this.chatboxForURL.delete(aURL);
|
|
}
|
|
cb = document.createElementNS("http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul", "chatbox");
|
|
// _callbacks is a javascript property instead of a <field> as it
|
|
// must exist before the (possibly delayed) bindings are created.
|
|
cb._callbacks = [aCallback];
|
|
// src also a javascript property; the src attribute is set in the ctor.
|
|
cb.src = aURL;
|
|
if (aMode == "minimized")
|
|
cb.setAttribute("minimized", "true");
|
|
cb.setAttribute("origin", aProvider.origin);
|
|
this.insertBefore(cb, this.firstChild);
|
|
this.selectedChat = cb;
|
|
this.chatboxForURL.set(aURL, Cu.getWeakReference(cb));
|
|
this.resize();
|
|
]]></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>
|
|
|
|
</implementation>
|
|
<handlers>
|
|
<handler event="popupshown"><![CDATA[
|
|
this.nub.removeAttribute("activity");
|
|
]]></handler>
|
|
<handler event="load"><![CDATA[
|
|
window.addEventListener("resize", this);
|
|
]]></handler>
|
|
<handler event="unload"><![CDATA[
|
|
window.removeEventListener("resize", this);
|
|
]]></handler>
|
|
</handlers>
|
|
</binding>
|
|
|
|
</bindings>
|