treestyletab/modules/fullTooltip.js

675 lines
20 KiB
JavaScript
Raw Normal View History

2014-09-30 07:09:27 -04:00
/* ***** BEGIN LICENSE BLOCK *****
* Version: MPL 1.1/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Mozilla Public License Version
* 1.1 (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
* http://www.mozilla.org/MPL/
*
* Software distributed under the License is distributed on an "AS IS" basis,
* WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
* for the specific language governing rights and limitations under the
* License.
*
* The Original Code is the Tree Style Tab.
*
* The Initial Developer of the Original Code is YUKI "Piro" Hiroshi.
2016-02-15 10:10:59 -05:00
* Portions created by the Initial Developer are Copyright (C) 2011-2016
2014-09-30 07:09:27 -04:00
* the Initial Developer. All Rights Reserved.
*
* Contributor(s): YUKI "Piro" Hiroshi <piro.outsider.reflex@gmail.com>
* Tetsuharu OHZEKI <https://github.com/saneyuki>
*
* Alternatively, the contents of this file may be used under the terms of
* either the GNU General Public License Version 2 or later (the "GPL"), or
* the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the MPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the MPL, the GPL or the LGPL.
*
* ***** END LICENSE BLOCK ******/
var EXPORTED_SYMBOLS = ['FullTooltipManager'];
2014-09-30 07:09:27 -04:00
const Cc = Components.classes;
const Ci = Components.interfaces;
Components.utils.import('resource://gre/modules/XPCOMUtils.jsm');
Components.utils.import('resource://treestyletab-modules/lib/inherit.jsm');
Components.utils.import('resource://treestyletab-modules/base.js');
Components.utils.import('resource://treestyletab-modules/pseudoTreeBuilder.js');
XPCOMUtils.defineLazyModuleGetter(this, 'utils', 'resource://treestyletab-modules/utils.js', 'TreeStyleTabUtils');
XPCOMUtils.defineLazyServiceGetter(this, 'ScreenManager',
'@mozilla.org/gfx/screenmanager;1', 'nsIScreenManager');
2016-02-18 09:12:24 -05:00
function log(...aArgs) {
utils.log.apply(utils, ['fullTooltip'].concat(aArgs));
}
function logWithStackTrace(...aArgs) {
utils.logWithStackTrace.apply(utils, ['fullTooltip'].concat(aArgs));
}
2014-09-30 07:09:27 -04:00
function FullTooltipManager(aOwner)
{
this.init(aOwner);
}
FullTooltipManager.prototype = inherit(TreeStyleTabBase, {
kTOOLTIP_MODE_DEFAULT : 0,
kTOOLTIP_MODE_COLLAPSED : 1,
kTOOLTIP_MODE_ALWAYS : 2,
get window()
{
return this.owner.window;
},
get document()
{
return this.owner.document;
},
get tabTooltip()
{
return this.document.getElementById('tabbrowser-tab-tooltip');
},
get tabFullTooltip()
{
return this.document.getElementById('treestyletab-full-tree-tooltip');
},
get container()
{
return this.tabFullTooltip.lastChild._scrollbox;
},
get tree()
{
return this.tabFullTooltip.lastChild.lastChild.lastChild;
},
2014-09-30 07:09:27 -04:00
/**
* If the window is maximized, screenX and screenY can be out of
* visible screen rect. On the other hand,
* PopupBoxObject#openPopupAtScreen() automatically reposition
* the popup if it is going to be shown out of the visible screen
* rect. As the result, the popup will be repositioned unexpectedly
* if I use the raw screenX and screenY.
* https://github.com/piroor/treestyletab/issues/302
* To prevent such a result, I have to calculate valid base position
* for the popup.
*/
get windowBasePosition() {
var screen = ScreenManager.screenForRect(
this.window.screenX,
this.window.screenY,
this.window.outerWidth,
this.window.outerHeight
);
var screenMinX = {},
screenMinY = {},
screenMaxX = {},
screenMaxY = {};
screen.GetAvailRect(screenMinX, screenMinY, screenMaxX, screenMaxY);
return {
x: Math.max(this.window.screenX, screenMinX.value),
y: Math.max(this.window.screenY, screenMinY.value)
};
},
getCurrentScreen : function FTM_getCurrentScreen(aBox)
{
var currentScreen = Cc['@mozilla.org/gfx/screenmanager;1']
.getService(Ci.nsIScreenManager)
.screenForRect(aBox.screenX, aBox.screenY, aBox.width, aBox.height);
var screenLeft = {},
screenTop = {},
screenWidth = {},
screenHeight = {};
currentScreen.GetRect(screenLeft, screenTop, screenWidth, screenHeight);
return {
left : screenLeft.value,
top : screenTop.value,
width : screenWidth.value,
height : screenHeight.value,
allowedWidth : Math.ceil(screenWidth.value * 0.8),
allowedHeight : Math.ceil(screenHeight.value * 0.7)
};
},
get currentTooltipMargins()
{
var tooltip = this.tabFullTooltip;
var style = this.window.getComputedStyle(tooltip, null);
return {
left : Number(style.getPropertyValue('margin-left').replace(/px/, '')),
top : Number(style.getPropertyValue('margin-top').replace(/px/, ''))
};
},
getFullTooltipFromEvent : function FTM_getFullTooltipFromEvent(aEvent)
{
return utils.evaluateXPath(
'ancestor-or-self::xul:tooltip[@id="'+this.tabFullTooltip.id+'"]',
aEvent.target,
Ci.nsIDOMXPathResult.FIRST_ORDERED_NODE_TYPE
).singleNodeValue;
},
2014-09-30 07:09:27 -04:00
init : function FTM_init(aOwner)
{
this.owner = aOwner;
this.tabTooltip.addEventListener('popupshowing', this, true);
this.tabTooltip.addEventListener('popuphiding', this, true);
this.tabFullTooltip.addEventListener('click', this, false);
this.tabFullTooltip.addEventListener(PseudoTreeBuilder.kTAB_LINK_CLICK, this, true);
this.tabFullTooltip.addEventListener('popupshown', this, true);
this.tabFullTooltip.addEventListener('popuphidden', this, true);
this.tabFullTooltip.addEventListener('transitionend', this, true);
2014-09-30 07:09:27 -04:00
},
destroy : function FTM_destroy()
{
this.cancel();
this.stopListenTooltipEvents();
this.tabTooltip.removeEventListener('popupshowing', this, true);
this.tabTooltip.removeEventListener('popuphiding', this, true);
this.tabFullTooltip.removeEventListener('click', this, false);
this.tabFullTooltip.removeEventListener(PseudoTreeBuilder.kTAB_LINK_CLICK, this, true);
this.tabFullTooltip.removeEventListener('popupshown', this, true);
this.tabFullTooltip.removeEventListener('popuphidden', this, true);
this.tabFullTooltip.removeEventListener('transitionend', this, true);
2014-09-30 07:09:27 -04:00
delete this.owner;
},
handleEvent : function FTM_handleEvent(aEvent)
{
switch (aEvent.type)
{
case 'click':
return this.onClick(aEvent);
case PseudoTreeBuilder.kTAB_LINK_CLICK:
return this.onItemClick(aEvent);
case 'popupshowing':
return this.onDefaultTooltipShowing(aEvent);
case 'popuphiding':
return this.onDefaultTooltipHiding(aEvent);
case 'popupshown':
return this.onShown(aEvent);
case 'popuphidden':
return this.onHidden(aEvent);
case 'mousemove':
if (this.changingPropertiesCount === 0)
this.onTooltipMouseMove(aEvent);
return;
2014-09-30 07:09:27 -04:00
case 'mouseover':
if (this.changingPropertiesCount === 0)
this.cancelDelayedHide();
return;
2014-09-30 07:09:27 -04:00
case 'mouseout':
if (this.changingPropertiesCount === 0)
this.hideWithDelay();
return;
2014-09-30 07:09:27 -04:00
case 'transitionend':
return this.onExpanded(aEvent);
2014-09-30 07:09:27 -04:00
default:
if (this.changingPropertiesCount === 0)
this.onTooltipEvent(aEvent);
return;
2014-09-30 07:09:27 -04:00
}
},
onClick : function FTM_onClick(aEvent)
{
this.tabFullTooltip.hidePopup();
},
onItemClick : function FTM_onItemClick(aEvent)
{
var id = aEvent.detail.id;
if (id) {
let tab = this.getTabById(id, this.owner.browser);
if (tab) {
let event = aEvent.detail.sourceEvent;
if (event.button == 1 ||
(event.button == 0 && this.isAccelKeyPressed(event)))
this.owner.browser.removeTab(tab);
else if (event.button != 2)
this.owner.browser.selectedTab = tab;
}
}
this.tabFullTooltip.hidePopup();
},
onDefaultTooltipShowing : function FTM_onDefaultTooltipShowing(aEvent)
{
this.cancel();
this.handleDefaultTooltip(aEvent);
},
onDefaultTooltipHiding : function FTM_onDefaultTooltipHiding(aEvent)
{
this.cancel();
},
onShown : function FTM_onShown(aEvent)
{
2016-02-18 09:12:24 -05:00
log('onShown');
2014-09-30 07:09:27 -04:00
this.startListenTooltipEvents();
this.expandTooltip();
2014-09-30 07:09:27 -04:00
},
onHidden : function FTM_onHidden(aEvent)
{
this.tabFullTooltip.removeAttribute('popup-shown');
this.stopListenTooltipEvents();
this.clear();
2014-09-30 07:09:27 -04:00
},
onTooltipMouseMove : function FTM_onTooltipMouseMove(aEvent)
{
if (this.getFullTooltipFromEvent(aEvent))
this.cancelDelayedHide();
else
this.hideWithDelay();
},
onTooltipEvent : function FTM_onTooltipEvent(aEvent)
{
if (!this.getFullTooltipFromEvent(aEvent))
this.hide();
},
startListenTooltipEvents : function FTM_startListenTooltipEvents()
{
if (this.listening)
return;
this.window.addEventListener('DOMMouseScroll', this, true);
this.window.addEventListener('keydown', this, true);
this.window.addEventListener('mousedown', this, true);
this.window.addEventListener('mouseup', this, true);
this.window.addEventListener('dragstart', this, true);
this.window.addEventListener('mousemove', this, true);
this.tabFullTooltip.addEventListener('mouseover', this, false);
this.tabFullTooltip.addEventListener('mouseout', this, false);
this.listening = true;
},
stopListenTooltipEvents : function FTM_stopListenTooltipEvents()
{
if (!this.listening)
return;
this.window.removeEventListener('DOMMouseScroll', this, true);
this.window.removeEventListener('keydown', this, true);
this.window.removeEventListener('mousedown', this, true);
this.window.removeEventListener('mouseup', this, true);
this.window.removeEventListener('dragstart', this, true);
this.window.removeEventListener('mousemove', this, true);
this.tabFullTooltip.removeEventListener('mouseover', this, false);
this.tabFullTooltip.removeEventListener('mouseout', this, false);
this.listening = false;
},
handleDefaultTooltip : function FTM_handleDefaultTooltip(aEvent)
{
2016-02-18 09:12:24 -05:00
log('handleDefaultTooltip');
2014-09-30 07:09:27 -04:00
var tab = this.getTabFromChild(this.document.tooltipNode);
if (!tab || tab.localName != 'tab')
return;
var label;
var collapsed = this.isSubtreeCollapsed(tab);
var mode = utils.getTreePref('tooltip.mode');
var base = parseInt(tab.getAttribute(this.kNEST) || 0);
var descendant = this.getDescendantTabs(tab);
var indentPart = ' ';
var tree = null;
var fullTooltipExtraLabel = '';
if (mode > this.kTOOLTIP_MODE_DEFAULT &&
descendant.length) {
let tabs = [tab].concat(descendant);
let tabsToBeListed = tabs.slice(0, Math.max(1, utils.getTreePref('tooltip.maxCount')));
tree = tabsToBeListed
.map(function(aTab) {
let label = aTab.getAttribute('label');
let indent = '';
let nest = parseInt(aTab.getAttribute(this.kNEST) || 0) - base;
for (let i = 0; i < nest; i++)
{
indent += indentPart;
}
return utils.treeBundle.getFormattedString('tooltip.item.label', [label, indent]);
}, this)
.join('\n');
if (tabs.length != tabsToBeListed.length) {
tree += '\n'+indentPart+utils.treeBundle.getFormattedString('tooltip.more', [tabs.length-tabsToBeListed.length]);
}
}
var shouldShowTree = mode != this.kTOOLTIP_MODE_DEFAULT && (collapsed || mode == this.kTOOLTIP_MODE_ALWAYS);
if ('mOverCloseButton' in tab && tab.mOverCloseButton) {
if (descendant.length &&
(collapsed || utils.getTreePref('closeParentBehavior') == this.kCLOSE_PARENT_BEHAVIOR_CLOSE_ALL_CHILDREN)) {
label = tree || tab.getAttribute('label');
label = label && shouldShowTree ?
utils.treeBundle.getFormattedString('tooltip.closeTree.labeled', [label]) :
utils.treeBundle.getString('tooltip.closeTree') ;
fullTooltipExtraLabel = utils.treeBundle.getFormattedString('tooltip.closeTree.labeled', ['%TREE%']).split(/\s*%TREE%\s*/);
}
}
else if (tab.getAttribute(this.kTWISTY_HOVER) == 'true') {
let key = collapsed ?
'tooltip.expandSubtree' :
'tooltip.collapseSubtree' ;
label = tree || tab.getAttribute('label');
label = label && shouldShowTree ?
utils.treeBundle.getFormattedString(key+'.labeled', [label]) :
utils.treeBundle.getString(key) ;
fullTooltipExtraLabel = utils.treeBundle.getFormattedString(key+'.labeled', ['%TREE%']).split(/\s*%TREE%\s*/);
}
else if (shouldShowTree) {
label = tree;
}
if (!label)
return;
aEvent.target.setAttribute('label', label);
aEvent.stopPropagation();
if (shouldShowTree)
this.setup(aEvent.target, tab, fullTooltipExtraLabel);
},
setup : function FTM_setup(aBaseTooltip, aTab, aExtraLabels)
{
2016-02-18 09:12:24 -05:00
log('setup');
2014-09-30 07:09:27 -04:00
this.cancel();
var delay = utils.getTreePref('tooltip.fullTooltipDelay');
if (delay < 0)
return;
this._fullTooltipTimer = this.window.setTimeout((function() {
log('setup:delayed');
2016-02-19 03:45:13 -05:00
var basePosition = this.windowBasePosition;
2016-02-19 03:45:13 -05:00
log(' => window position: ', basePosition);
2014-09-30 07:09:27 -04:00
var box = aBaseTooltip.boxObject;
var x = box.screenX - basePosition.x;
var y = box.screenY - basePosition.y;
var w = box.width;
var h = box.height;
log(' => base tooltip: ', {
screenX : box.screenX,
screenY : box.screenY,
x : x,
y : y,
width : w,
height : h
});
2016-02-19 03:45:13 -05:00
aBaseTooltip.hidePopup();
2014-09-30 07:09:27 -04:00
this.fill(aTab, aExtraLabels);
2014-09-30 07:09:27 -04:00
var tooltip = this.tabFullTooltip;
2015-02-26 10:59:31 -05:00
{
2016-02-19 06:48:39 -05:00
this.setTooltipPosition(x, y);
2015-02-26 10:59:31 -05:00
let style = tooltip.style;
style.maxWidth = style.minWidth = w+'px';
2014-09-30 07:09:27 -04:00
style.maxHeight = style.minHeight = h+'px';
}
tooltip.openPopupAtScreen(basePosition.x, basePosition.y, false);
}).bind(this), Math.max(delay, 0), this);
2014-09-30 07:09:27 -04:00
},
2016-02-19 06:48:39 -05:00
setTooltipPosition : function FTM_setTooltipPosition(aX, aY, aScreen)
{
var tooltip = this.tabFullTooltip;
var style = tooltip.style;
var box = tooltip.boxObject;
var aScreen = aScreen || this.lastScreen || this.getCurrentScreen(box);
2016-02-19 06:48:39 -05:00
style.marginLeft = aX+'px';
style.marginTop = aY+'px';
},
2014-09-30 07:09:27 -04:00
cancel : function FTM_cancel()
2014-09-30 07:09:27 -04:00
{
if (this._fullTooltipTimer) {
this.window.clearTimeout(this._fullTooltipTimer);
this._fullTooltipTimer = null;
}
this.hide();
},
hide : function FTM_hide()
{
logWithStackTrace('hide');
2014-09-30 07:09:27 -04:00
this.cancelDelayedHide();
this.tabFullTooltip.hidePopup();
this.changingPropertiesCount = 0;
this.lastScreen = null;
2014-09-30 07:09:27 -04:00
},
hideWithDelay : function FTM_hideWithDelay()
{
this.cancelDelayedHide();
this._delayedHideTimer = this.window.setTimeout(function(aSelf) {
aSelf.hide();
}, 500, this);
},
cancelDelayedHide : function FTM_cancelDelayedHide()
{
if (this._delayedHideTimer) {
this.window.clearTimeout(this._delayedHideTimer);
this._delayedHideTimer = null;
}
},
fill : function FTM_fill(aTab, aExtraLabels)
{
2016-02-18 09:12:24 -05:00
log('fill');
2014-09-30 07:09:27 -04:00
this.clear();
var root = this.document.createElement('arrowscrollbox');
var orient = utils.getTreePref('tooltip.columnize') ? 'horizontal' : 'vertical' ;
root.setAttribute('orient', orient);
2014-09-30 07:09:27 -04:00
root.setAttribute('flex', 1);
2016-02-15 11:24:36 -05:00
var container = root.appendChild(this.document.createElement('vbox'));
2014-09-30 07:09:27 -04:00
if (aExtraLabels) {
if (typeof aExtraLabels == 'string')
aExtraLabels = [aExtraLabels];
for (let i = 0, maxi = aExtraLabels.length; i < maxi; i++)
{
let label = aExtraLabels[i];
label = label.trim();
if (!label)
continue;
let caption = this.document.createElement('description');
caption.appendChild(this.document.createTextNode(label));
container.appendChild(caption)
2014-09-30 07:09:27 -04:00
}
}
2016-02-18 09:12:24 -05:00
var tree = PseudoTreeBuilder.build(aTab);
2016-02-15 11:24:36 -05:00
container.insertBefore(tree, container.firstChild && container.firstChild.nextSibling);
root.appendChild(container);
2014-09-30 07:09:27 -04:00
this.tabFullTooltip.appendChild(root);
},
clear : function FTM_clear()
{
var range = this.document.createRange();
range.selectNodeContents(this.tabFullTooltip);
range.deleteContents();
range.detach();
},
expandTooltip : function FTM_expandTooltip()
{
log('expandTooltip');
var tooltip = this.tabFullTooltip;
2016-02-19 03:45:13 -05:00
{
let tree = this.tree;
2016-02-19 03:45:13 -05:00
let basePosition = this.windowBasePosition;
let tooltipBox = tooltip.boxObject;
log(' => initial dimension: ', {
2016-02-19 03:45:13 -05:00
screenX : tooltipBox.screenX,
screenY : tooltipBox.screenY,
x : tooltipBox.screenX - basePosition.x,
y : tooltipBox.screenY - basePosition.y,
width : tooltipBox.width,
height : tooltipBox.height,
tree : {
width : tree.clientWidth,
height : tree.clientHeight
}
2016-02-19 03:45:13 -05:00
});
}
this.lastScreen = this.getCurrentScreen(tooltip.boxObject);
this.determineTreeSize();
this.expandTooltipInternal();
},
determineTreeSize : function FTM_determineTreeSize()
{
var tree = this.tree;
var columnize = utils.getTreePref('tooltip.columnize');
if (columnize) {
PseudoTreeBuilder.columnizeTree(tree, {
2016-03-05 00:47:55 -05:00
columnWidth : utils.getTreePref('tooltip.columnize.width'),
containerBox : {
2016-03-04 08:26:51 -05:00
width : this.lastScreen.allowedWidth,
height : this.lastScreen.allowedHeight
},
calculateCount : true
});
}
var range = this.document.createRange();
range.selectNodeContents(tree);
if (tree.previousSibling)
range.setStartBefore(tree.previousSibling);
var rect = range.getBoundingClientRect();
range.detach();
var container = this.container;
var arrowScrollBox = container.parentNode;
var scrollButtonsMargin = (arrowScrollBox.boxObject.width - arrowScrollBox._scrollbox.boxObject.width);
scrollButtonsMargin *= 2; // enough width to deactivate scroll buttons.
var containerStyle = container.style;
containerStyle.width = (container.width = rect.width + scrollButtonsMargin)+'px';
containerStyle.height = (container.height = rect.height)+'px';
},
expandTooltipInternal : function FTM_expandTooltipInternal()
{
log('expandTooltipInternal');
this.changingPropertiesCount = 2;
var tooltip = this.tabFullTooltip;
tooltip.setAttribute('popup-shown', true);
var currentScreen = this.lastScreen || this.getCurrentScreen(box);
var box = tooltip.boxObject;
var currentX = box.screenX - currentScreen.left;
var currentY = box.screenY - currentScreen.top;
var container = this.container;
var maxWidth = Math.min(container.width, currentScreen.allowedWidth);
var maxHeight = Math.min(container.height, currentScreen.allowedHeight);
var style = tooltip.style;
style.maxWidth = maxWidth+'px';
style.maxHeight = currentScreen.allowedHeight+'px';
style.minWidth = 0;
style.minHeight = 0;
var margins = this.currentTooltipMargins;
var maxX = currentScreen.width - maxWidth;
var maxY = currentScreen.height - maxHeight;
log(' => current dimension: ', {
margins : margins,
x : currentX,
y : currentY,
maxX : maxX,
maxY : maxY,
container : {
width : container.width,
height : container.height
},
screen : currentScreen,
columnCount : this.tree.columnCount
2016-02-19 04:36:42 -05:00
});
if (currentX > maxX)
margins.left -= currentX - maxX + Math.ceil(currentScreen.width * 0.05);
if (currentY > maxY)
margins.top -= currentY - maxY + Math.ceil(currentScreen.height * 0.05);
this.setTooltipPosition(margins.left, margins.top, currentScreen);
log(' => updated margins: ', margins);
},
onExpanded : function FTM_onExpanded(aEvent)
{
let tooltipBox = this.tabFullTooltip.boxObject;
let tree = this.tree;
log('transitionend: ', {
target : aEvent.target,
property : aEvent.propertyName,
value : this.window.getComputedStyle(aEvent.target, null)
.getPropertyValue(aEvent.propertyName),
tooltipSize : {
width : tooltipBox.width,
height : tooltipBox.height
},
treeSize : {
width : tree.clientWidth,
height : tree.clientHeight
}
});
if (this.changingPropertiesCount > 0 &&
/width|height/.test(aEvent.propertyName))
this.changingPropertiesCount--;
2014-09-30 07:09:27 -04:00
}
});