<?xml version='1.0'?>
<!DOCTYPE bindings [
<!ENTITY % translate SYSTEM "../translator.dtd">
%translate;
]>
<bindings xmlns="http://www.mozilla.org/xbl" xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">

  <binding id="button">

    <implementation implements="nsIDOMEventListener, nsIObserver">
        <field name="contextMenu">document.getElementById("contentAreaContextMenu")</field>

        <field name="tooltipHTML">document.getElementById("aHTMLTooltip")</field>

        <field name="_lastTimeStamp">0</field>
        <field name="_boundHideBalloonCb">null</field>

        <field name="content">gBrowser.mPanelContainer</field>

        <field name="_tooltip"/>
        <field name="tooltip"><![CDATA[this._tooltip || (this._tooltip = this.createTooltip());]]></field>

        <field name="_isMac"><![CDATA[this.module.API.Environment.os.name === "mac";]]></field>
        <field name="_deactivateTimeoutId">null</field> 
        <field name="_showBalloonTimeoutId">null</field> 

        <field name="_logger" readonly="true">this.module.API.logger</field>

        <field name="checked"/>
        <field name="_lastMouseMoveData"/>
        <field name="_lastCtrlKeydown"/> 
        <field name="_mouseEventSending"/>
        <field name="showTooltipTimeoutID"/>   

        <constructor><![CDATA[if (! this.module)
return;
["mousemove", "mouseout", "keydown", "scroll", "keyup"].forEach(function (eventType) {
this.content.addEventListener(eventType,this,false);
}
,this);
this.listenToVisibilityChange(true);
this._windowListener = this.module.API.Browser.getWindowListener(window);
this._windowListener.addListener("WindowLocationChange",this);
this._windowListener.addListener("PageHide",this);]]></constructor>

        <destructor><![CDATA[this.clearShowTimeout();
this.clearDeactivateTimeout();
["mousemove", "mouseout", "keydown", "scroll", "keyup"].forEach(function (eventType) {
this.content.removeEventListener(eventType,this,false);
}
,this);
if (this.request)
{
this.request.abort();
this.request = null;
}

this.hideBalloon();
this.listenToVisibilityChange(false);
if (this._tooltip)
{
this._tooltip.tooltipController = null;
this._tooltip = null;
}

if (this._windowListener)
{
this._windowListener.removeListener("WindowLocationChange",this);
this._windowListener.removeListener("PageHide",this);
}

if (this._htmlTooltipHider)
{
this.tooltipHTML.removeEventListener("popupshowing",this._htmlTooltipHider,true);
}
]]></destructor>

        <method name="listenToVisibilityChange">
            <parameter name="enable"/>
            <body><![CDATA[var visibilityChange;
var changeVisibilityEvents = ["deactivate"];
if (typeof document.hidden !== "undefined")
{
visibilityChange = "visibilitychange";
}
else
if (typeof document.mozHidden !== "undefined")
{
visibilityChange = "mozvisibilitychange";
}

if (visibilityChange)
changeVisibilityEvents.push(visibilityChange);
if (this._boundHideBalloonCb === null)
{
let timeout = this.module.API.Environment.os.name === "windows" ? 200 : 0;
this._boundHideBalloonCb = (function _boundHideBalloonCb() {
this.clearShowTimeout();
if (this._showBalloonTimeoutId)
window.clearTimeout(this._showBalloonTimeoutId);
this._deactivateTimeoutId = window.setTimeout((function _timedDeactivate() {
if (typeof this.hideBalloon === "function")
this.hideBalloon();
}
).bind(this),timeout);
}
).bind(this);
}

changeVisibilityEvents.forEach(function (eventType) {
if (enable)
window.addEventListener(eventType,this._boundHideBalloonCb,false); else
window.removeEventListener(eventType,this._boundHideBalloonCb,false);
}
,this);
if (! enable)
this._boundHideBalloonCb = null;]]></body>
        </method>

        <method name="createTooltip">
            <body><![CDATA[const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
var tooltip = document.createElementNS(XUL_NS,"tooltip");
tooltip.setAttribute("noautohide","true");
var osName = this.module.API.Environment.os.name;
tooltip.setAttribute("yaIsLinux",osName == "linux");
tooltip.style.MozBinding = "url('" + this.module.URI + "native/fx/translator/balloon.xbl#balloon')";
tooltip.tooltipController = this;
var self = this;
tooltip.addEventListener("popupshown",function () {
window.setTimeout(function () {
if (tooltip.state === "open")
{
self.module.API.Statistics.logShortAction(5510);
}

}
,100);
}
,false);
tooltip.addEventListener("popuphidden",function () {
tooltip.removeAttribute("data-translations");
}
,false);
document.getElementById("mainPopupSet").appendChild(tooltip);
this._htmlTooltipHider = function (event) {
if (self.isPopupOpen(self.tooltip))
{
event.preventDefault();
}

}
;
this.tooltipHTML.addEventListener("popupshowing",this._htmlTooltipHider,true);
return tooltip;]]></body>
        </method>

        <method name="tryTranslateNode">
            <parameter name="mouseMoveEvent"/>
            <body><![CDATA[var aNode = mouseMoveEvent.rangeParent;
if (! aNode || aNode.nodeType !== Node.TEXT_NODE)
return false;
try {
if (/^(chrome|about|yafd):/.test(aNode.ownerDocument.documentURI))
return false;
}
catch (e) {

}

var parent;
try {
parent = aNode.ownerDocument.elementFromPoint(mouseMoveEvent.clientX,mouseMoveEvent.clientY);
}
catch (e) {

}

if (! parent || aNode.parentNode !== parent)
return false;
var aOffset = mouseMoveEvent.rangeOffset;
var [text, begin, end] = this.module.getTranslatableTextFromString(aNode.nodeValue,aOffset);
if (this.module.isBadText(text) || ! this.isEventInRange(aNode,begin,end,mouseMoveEvent))
{
return false;
}

return this.translateIt({
text: text,
x: mouseMoveEvent.screenX + 1,
y: mouseMoveEvent.screenY + 1,
nodeRef: this.module._getNodeWeakReference(parent),
begin: begin,
end: end});]]></body>
        </method>

        <method name="translateIt">
            <parameter name="aBalloon"/>
            <body><![CDATA[if (this.isPopupOpen(this.tooltip) && this.balloon && this.balloon.text == aBalloon.text && this.balloon.begin == aBalloon.begin)
{
return true;
}

aBalloon = this.module._cache.restoreBalloon(aBalloon);
this.balloon = aBalloon;
this.__translateItCallTimestamp = Date.now();
if (aBalloon.translations)
{
this.showBalloon(aBalloon);
}
else
{
let isSeznamSlovnik = aBalloon.from == "cs" || aBalloon.to == "cs";
if (isSeznamSlovnik && ! this.module.storageSsid)
return true;
let addonLocale = this.module.API.Environment.addon.locale;
let localeLang = addonLocale && addonLocale.language || "en";
let sendData = null;
let baseURL = isSeznamSlovnik ? this.module.TRANSLATE_URL_CZ : this.module.TRANSLATE_URL;
let requestUrl = baseURL + "?text=" + encodeURIComponent(aBalloon.text) + "&from=all" + "&to=" + this.module.defaultLanguage + "&brid=" + this.module.brandId + "&locale=" + localeLang;
if (this.request)
{
if (this.request.url === requestUrl)
return false;
this.request.abort();
}

this.request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
this.request.url = requestUrl;
let target = this.request.QueryInterface(Ci.nsIDOMEventTarget);
target.addEventListener("load",this,false);
target.addEventListener("error",this,false);
if (isSeznamSlovnik)
{
let trlang = aBalloon.from.replace("cs","cz") + "_" + aBalloon.to.replace("cs","cz");
aBalloon.url = "";
sendData = this.module.xutils.xrpcRequest("toolbar.search",[aBalloon.text, trlang, 0, 4, this.module.storageSsid]);
}

this.request.open(isSeznamSlovnik ? "POST" : "GET",requestUrl,true);
if (isSeznamSlovnik)
{
this.request.setRequestHeader("Content-Type","text/xml");
}

this.request.send(sendData);
}

return true;]]></body>
        </method>

        <method name="response">
            <parameter name="event"/>
            <body><![CDATA[var request = event.target;
var status = request.status;
if (this.request === request)
{
this.request = null;
}

if (status < 200 || status > 399)
{
return;
}

var rxml = request.responseXML;
var balloon = this.balloon;
if (balloon.from === "cs" || balloon.to === "cs")
{
var obj = this.module.xutils.xrpcResponse(rxml)[0];
if (! obj || obj.status != 200 || ! obj.translations || ! obj.translations.length)
{
return;
}

balloon.translations = obj.translations.map(function (t) {
return t.translatedPhrase;
}
);
balloon.translations.unshift(obj.translations[0].phrase);
balloon.url = "http://slovnik.seznam.cz/" + balloon.from.replace("cs","cz") + "-" + balloon.to.replace("cs","cz") + "/word/?q=" + encodeURIComponent(balloon.text);
}
else
{
let balloon = this.balloon;
let errorElement = rxml.querySelector("error");
let errorStatus = parseInt(errorElement && errorElement.getAttribute("status"),10);
if (errorStatus !== 0)
{
this.hideBalloon();
return;
}

balloon.to = rxml.querySelector("to").textContent;
balloon.request = rxml.querySelector("request").textContent;
balloon.translations = {
};
Array.forEach(rxml.querySelectorAll("translations[from]"),function (translations) {
var lang = translations.getAttribute("from");
balloon.translations[lang] = [];
Array.forEach(translations.querySelectorAll("translation"),function (translation) {
var word = translation.getAttribute("word");
var part = {
};
var parts = [];
Array.forEach(translation.querySelectorAll("variant"),function (variant) {
var partOfSpeech = variant.getAttribute("class");
if (parts.indexOf(partOfSpeech) === - 1)
{
parts.push(partOfSpeech);
part[partOfSpeech] = [];
}

var variantStr = variant.textContent;
if (part[partOfSpeech].indexOf(variantStr) === - 1)
{
part[partOfSpeech].push(variantStr);
}

}
);
if (parts.length)
{
balloon.translations[lang].push({
word: word,
parts: parts,
part: part});
}

}
);
var url = translations.querySelector("url");
if (url)
{
balloon.translations[lang].url = url.textContent;
}

}
);
}

this.module._cache.storeBalloon(balloon);
if (request.doNotShow)
{
return;
}

this.showBalloon(balloon);]]></body>
        </method>

        <method name="showBalloon">
            <parameter name="balloon"/>
            <body><![CDATA[if (! balloon.translations || Object.keys(balloon.translations).length === 0)
{
this.hideBalloon();
return;
}

var show = (function show() {
if (this.isPopupOpen(this.contextMenu))
{
this.hideBalloon();
return;
}

if (this.isPopupOpen(this.tooltipHTML))
{
this.tooltipHTML.hidePopup();
}

var tooltip = this.tooltip;
if (tooltip.getAttribute("data-translations") === JSON.stringify(balloon.translations))
return;
tooltip.clearHideTimeout();
tooltip.hidePopup();
tooltip.setAttribute("data-translations",JSON.stringify(balloon.translations));
var langs = Object.keys(balloon.translations);
var currentURI = gBrowser.mCurrentBrowser.currentURI.spec;
var contentDocument = gBrowser.mCurrentBrowser.contentDocument;
var pageLang = this.module.getPageLanguage(currentURI,contentDocument);
langs.active = langs.indexOf(pageLang) !== - 1 ? pageLang : langs[0];
tooltip.langs = langs;
tooltip.translations = balloon.translations;
tooltip.pageZoom = gBrowser.selectedBrowser.markupDocumentViewer.fullZoom;
tooltip.openAtY = balloon.y;
tooltip.openPopupAtScreen(balloon.x,balloon.y,false);
}
).bind(this);
if (this.__translateItCallTimestamp)
{
let timeDiff = Date.now() + this.module.intervalShow - this.__translateItCallTimestamp;
this.__translateItCallTimestamp = null;
if (timeDiff < this.module.minShowBalloonInterval)
{
this._showBalloonTimeoutId = setTimeout(show,this.module.minShowBalloonInterval - timeDiff + 20);
return;
}

}

show();]]></body>
        </method>

        <method name="doNotShowBalloonNow">
            <body><![CDATA[this.clearShowTimeout();
if (this.request)
this.request.doNotShow = true;]]></body>
        </method>

        <method name="hideBalloon">
            <body><![CDATA[if (this._tooltip && this._tooltip.hidePopup)
{
this.tooltip.clearHideTimeout();
this._tooltip.hidePopup();
}

if (this.balloon && this.balloon.nodeRef)
{
this.balloon.nodeRef = null;
}
]]></body>
        </method>

        <method name="clearShowTimeout">
            <body><![CDATA[if (! (this.showTooltipTimeoutID || this._showBalloonTimeoutId))
return;
clearTimeout(this.showTooltipTimeoutID);
clearTimeout(this._showBalloonTimeoutId);
this.showTooltipTimeoutID = null;
this._showBalloonTimeoutId = null;]]></body>
        </method>

        <method name="clearDeactivateTimeout">
            <body><![CDATA[if (! this._deactivateTimeoutId)
return;
clearTimeout(this._deactivateTimeoutId);
this._deactivateTimeoutId = null;]]></body>
        </method>

        <method name="checkForNewNode">
            <parameter name="aRangeParent"/>
            <parameter name="aRangeOffset"/>
            <body><![CDATA[if (this._checkForNewNode(aRangeParent,aRangeOffset))
{
this.hideBalloon();
}
]]></body>
        </method>

        <method name="_checkForNewNode">
            <parameter name="aRangeParent"/>
            <parameter name="aRangeOffset"/>
            <body><![CDATA[if (! this.isPopupOpen(this.tooltip) || ! this.balloon)
{
return false;
}

return aRangeOffset < this.balloon.begin || aRangeOffset > this.balloon.end;]]></body>
        </method>

        <method name="sendMouseEvent">
            <parameter name="aSelf"/>
            <body><![CDATA[var that = aSelf || this;
var mouseData = that._lastMouseMoveData;
that.clearShowTimeout();
if (! mouseData)
return;
that._sendMouseEvent(mouseData.x,mouseData.y,mouseData.viewRef);]]></body>
        </method>

        <method name="_sendMouseEvent">
            <parameter name="aClientX"/>
            <parameter name="aClientY"/>
            <parameter name="aViewRef"/>
            <body><![CDATA[var view = this._getReferencee(aViewRef);
if (! view)
return;
try {
let node = view.document.elementFromPoint(aClientX,aClientY);
if (node && /\[object XrayWrapper \[object HTML(Object|Embed)Element\]\]/.test(String(node)))
return;
}
catch (e) {

}

this._mouseEventSending = true;
try {
let domWindowUtils = view.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
domWindowUtils.sendMouseEvent("mousemove",aClientX,aClientY,0,0,0);
}
catch (e) {

}

this._mouseEventSending = false;]]></body>
        </method>

        <method name="_getReferencee">
            <parameter name="aWeakRef"/>
            <body><![CDATA[try {
return aWeakRef.QueryReferent(Ci.nsISupports);
}
catch (e) {

}

return null;]]></body>
        </method>

        <method name="isEventInRange">
            <parameter name="aNode"/>
            <parameter name="aStart"/>
            <parameter name="aEnd"/>
            <parameter name="aEvent"/>
            <body><![CDATA[var nodeRange = aNode.ownerDocument.createRange();
nodeRange.setStart(aNode,aStart);
nodeRange.setEnd(aNode,aEnd);
var nodeBCR = nodeRange.getBoundingClientRect();
if (nodeBCR.left > aEvent.clientX || nodeBCR.right < aEvent.clientX || nodeBCR.top > aEvent.clientY || nodeBCR.bottom < aEvent.clientY)
return false;
return true;]]></body>
        </method>

        
        <method name="isPopupOpen">
            <parameter name="aPopupNode"/>
            <body><![CDATA[return aPopupNode && ["open", "showing"].indexOf(aPopupNode.state || "") > - 1;]]></body>
        </method>

        
        <method name="shouldTranslate">
            <parameter name="aEvent"/>
            <body><![CDATA[if (this.module.translateOnHover)
{
return true;
}

if (! aEvent)
{
return false;
}

var translateKeyPref = this.module.translateOnHoverOption;
var isMac = this._isMac;
var ctrlKey = isMac ? "metaKey" : "ctrlKey", shiftKey = "shiftKey";
var ctrlKeyConst = this.module.TRANSLATE_ON_CTRLKEY, shiftKeyConst = this.module.TRANSLATE_ON_SHIFTKEY;
if (translateKeyPref == ctrlKeyConst || translateKeyPref == this.shiftKeyConst)
{
if (aEvent[ctrlKey] && aEvent.shiftKey)
{
return false;
}

}

if (translateKeyPref == ctrlKeyConst)
{
if (aEvent instanceof Components.interfaces.nsIDOMMouseEvent)
{
return aEvent[ctrlKey];
}

if (isMac)
{
return aEvent.keyCode === aEvent.DOM_VK_META;
}
else
{
return aEvent.keyCode === aEvent.DOM_VK_CONTROL;
}

}
else
if (translateKeyPref == shiftKeyConst)
{
if (aEvent instanceof Components.interfaces.nsIDOMMouseEvent)
{
return aEvent[shiftKey];
}

return aEvent.keyCode === aEvent.DOM_VK_SHIFT;
}
else
{
this._logger.debug("Translate modifier key preference value is inconsistent.");
}

return false;]]></body>
        </method>

        <method name="handleEvent">
            <parameter name="event"/>
            <body><![CDATA[var target = event.originalTarget;
switch (event.type) {
case "mousemove":
if (this.isPopupOpen(this.tooltip) && this.tooltip._mouseOver)
return;
if (this._mouseEventSending)
{
event.stopPropagation();
if (! this.tryTranslateNode(event))
{
this.hideBalloon();
}

}
else
{
let lastX = this._lastMouseMoveData && this._lastMouseMoveData.x;
let lastY = this._lastMouseMoveData && this._lastMouseMoveData.y;
let dX = lastX !== undefined ? Math.abs(event.clientX - lastX) : 0;
let dY = lastY !== undefined ? Math.abs(event.clientY - lastY) : 0;
if (dX < 3 && dY < 3)
return;
this._lastMouseMoveData = {
x: event.clientX,
y: event.clientY,
viewRef: this.module._getNodeWeakReference(event.view)};
if (this.shouldTranslate(event))
{
this._lastTimeStamp = event.timeStamp;
this.clearShowTimeout();
this.showTooltipTimeoutID = setTimeout(this.sendMouseEvent,this.module.intervalShow,this);
}
else
{
this.showTooltipTimeoutID = setTimeout(function (that) {
if (! (that._tooltip && that._tooltip._mouseOver))
{
that.hideBalloon();
}

}
,this.module.intervalShow,this);
}

}

break;
case "mouseout":
this._lastMouseMoveData = null;
if (! this.isPopupOpen(this.tooltip))
this.doNotShowBalloonNow();
break;
case "keydown":
if (this.shouldTranslate())
{
if (this.isPopupOpen(this.tooltip))
{
this.hideBalloon();
}

return;
}

if (! this.shouldTranslate(event))
{
this.doNotShowBalloonNow();
this.hideBalloon();
return;
}

if (this._lastCtrlKeydown)
return;
this._lastCtrlKeydown = Date.now();
this.clearShowTimeout();
this.showTooltipTimeoutID = setTimeout(this.sendMouseEvent,this.module.intervalShow,this);
break;
case "keyup":
if (this.shouldTranslate() || ! this.shouldTranslate(event))
{
return;
}

this._lastCtrlKeydown = null;
if (this.request && ! this.request.doNotShow)
{
this.clearShowTimeout();
this.sendMouseEvent();
}

break;
case "scroll":
this.doNotShowBalloonNow();
this.hideBalloon();
break;
case "load":
this.response(event);
break;
case "error":
break;
default:
break;
}
]]></body>
        </method>

        <method name="observe">
            <parameter name="aSubject"/>
            <parameter name="aTopic"/>
            <parameter name="aData"/>
            <body><![CDATA[switch (aTopic) {
case "PageHide":

case "onWindowLocationChange":
this.doNotShowBalloonNow();
this.hideBalloon();
break;
default:
break;
}
]]></body>
        </method>
    </implementation>
  </binding>
</bindings>
