Files
UXP-Fixed/devtools/shared/gcli/source/lib/gcli/ui/focus.js
T
2018-02-02 04:16:08 -05:00

404 lines
11 KiB
JavaScript

/*
* Copyright 2012, Mozilla Foundation and contributors
*
* Licensed under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
'use strict';
var util = require('../util/util');
var l10n = require('../util/l10n');
/**
* Record how much help the user wants from the tooltip
*/
var Eagerness = {
NEVER: 1,
SOMETIMES: 2,
ALWAYS: 3
};
/**
* Export the eagerHelper setting
*/
exports.items = [
{
item: 'setting',
name: 'eagerHelper',
type: {
name: 'selection',
lookup: [
{ name: 'never', value: Eagerness.NEVER },
{ name: 'sometimes', value: Eagerness.SOMETIMES },
{ name: 'always', value: Eagerness.ALWAYS }
]
},
defaultValue: Eagerness.SOMETIMES,
description: l10n.lookup('eagerHelperDesc'),
ignoreTypeDifference: true
}
];
/**
* FocusManager solves the problem of tracking focus among a set of nodes.
* The specific problem we are solving is when the hint element must be visible
* if either the command line or any of the inputs in the hint element has the
* focus, and invisible at other times, without hiding and showing the hint
* element even briefly as the focus changes between them.
* It does this simply by postponing the hide events by 250ms to see if
* something else takes focus.
*/
function FocusManager(document, settings) {
if (document == null) {
throw new Error('document == null');
}
this.document = document;
this.settings = settings;
this.debug = false;
this.blurDelay = 150;
this.window = this.document.defaultView;
this._blurDelayTimeout = null; // Result of setTimeout in delaying a blur
this._monitoredElements = []; // See addMonitoredElement()
this._isError = false;
this._hasFocus = false;
this._helpRequested = false;
this._recentOutput = false;
this.onVisibilityChange = util.createEvent('FocusManager.onVisibilityChange');
this._focused = this._focused.bind(this);
if (this.document.addEventListener) {
this.document.addEventListener('focus', this._focused, true);
}
var eagerHelper = this.settings.get('eagerHelper');
eagerHelper.onChange.add(this._eagerHelperChanged, this);
this.isTooltipVisible = undefined;
this.isOutputVisible = undefined;
this._checkShow();
}
/**
* Avoid memory leaks
*/
FocusManager.prototype.destroy = function() {
var eagerHelper = this.settings.get('eagerHelper');
eagerHelper.onChange.remove(this._eagerHelperChanged, this);
this.document.removeEventListener('focus', this._focused, true);
for (var i = 0; i < this._monitoredElements.length; i++) {
var monitor = this._monitoredElements[i];
console.error('Hanging monitored element: ', monitor.element);
monitor.element.removeEventListener('focus', monitor.onFocus, true);
monitor.element.removeEventListener('blur', monitor.onBlur, true);
}
if (this._blurDelayTimeout) {
this.window.clearTimeout(this._blurDelayTimeout);
this._blurDelayTimeout = null;
}
this._focused = undefined;
this.document = undefined;
this.settings = undefined;
this.window = undefined;
};
/**
* The easy way to include an element in the set of things that are part of the
* aggregate focus. Using [add|remove]MonitoredElement() is a simpler way of
* option than calling report[Focus|Blur]()
* @param element The element on which to track focus|blur events
* @param where Optional source string for debugging only
*/
FocusManager.prototype.addMonitoredElement = function(element, where) {
if (this.debug) {
console.log('FocusManager.addMonitoredElement(' + (where || 'unknown') + ')');
}
var monitor = {
element: element,
where: where,
onFocus: function() { this._reportFocus(where); }.bind(this),
onBlur: function() { this._reportBlur(where); }.bind(this)
};
element.addEventListener('focus', monitor.onFocus, true);
element.addEventListener('blur', monitor.onBlur, true);
if (this.document.activeElement === element) {
this._reportFocus(where);
}
this._monitoredElements.push(monitor);
};
/**
* Undo the effects of addMonitoredElement()
* @param element The element to stop tracking
* @param where Optional source string for debugging only
*/
FocusManager.prototype.removeMonitoredElement = function(element, where) {
if (this.debug) {
console.log('FocusManager.removeMonitoredElement(' + (where || 'unknown') + ')');
}
this._monitoredElements = this._monitoredElements.filter(function(monitor) {
if (monitor.element === element) {
element.removeEventListener('focus', monitor.onFocus, true);
element.removeEventListener('blur', monitor.onBlur, true);
return false;
}
return true;
});
};
/**
* Monitor for new command executions
*/
FocusManager.prototype.updatePosition = function(dimensions) {
var ev = {
tooltipVisible: this.isTooltipVisible,
outputVisible: this.isOutputVisible,
dimensions: dimensions
};
this.onVisibilityChange(ev);
};
/**
* Monitor for new command executions
*/
FocusManager.prototype.outputted = function() {
this._recentOutput = true;
this._helpRequested = false;
this._checkShow();
};
/**
* We take a focus event anywhere to be an indication that we might be about
* to lose focus
*/
FocusManager.prototype._focused = function() {
this._reportBlur('document');
};
/**
* Some component has received a 'focus' event. This sets the internal status
* straight away and informs the listeners
* @param where Optional source string for debugging only
*/
FocusManager.prototype._reportFocus = function(where) {
if (this.debug) {
console.log('FocusManager._reportFocus(' + (where || 'unknown') + ')');
}
if (this._blurDelayTimeout) {
if (this.debug) {
console.log('FocusManager.cancelBlur');
}
this.window.clearTimeout(this._blurDelayTimeout);
this._blurDelayTimeout = null;
}
if (!this._hasFocus) {
this._hasFocus = true;
}
this._checkShow();
};
/**
* Some component has received a 'blur' event. This waits for a while to see if
* we are going to get any subsequent 'focus' events and then sets the internal
* status and informs the listeners
* @param where Optional source string for debugging only
*/
FocusManager.prototype._reportBlur = function(where) {
if (this.debug) {
console.log('FocusManager._reportBlur(' + where + ')');
}
if (this._hasFocus) {
if (this._blurDelayTimeout) {
if (this.debug) {
console.log('FocusManager.blurPending');
}
return;
}
this._blurDelayTimeout = this.window.setTimeout(function() {
if (this.debug) {
console.log('FocusManager.blur');
}
this._hasFocus = false;
this._checkShow();
this._blurDelayTimeout = null;
}.bind(this), this.blurDelay);
}
};
/**
* The setting has changed
*/
FocusManager.prototype._eagerHelperChanged = function() {
this._checkShow();
};
/**
* The terminal tells us about keyboard events so we can decide to delay
* showing the tooltip element
*/
FocusManager.prototype.onInputChange = function() {
this._recentOutput = false;
this._checkShow();
};
/**
* Generally called for something like a F1 key press, when the user explicitly
* wants help
*/
FocusManager.prototype.helpRequest = function() {
if (this.debug) {
console.log('FocusManager.helpRequest');
}
this._helpRequested = true;
this._recentOutput = false;
this._checkShow();
};
/**
* Generally called for something like a ESC key press, when the user explicitly
* wants to get rid of the help
*/
FocusManager.prototype.removeHelp = function() {
if (this.debug) {
console.log('FocusManager.removeHelp');
}
this._importantFieldFlag = false;
this._isError = false;
this._helpRequested = false;
this._recentOutput = false;
this._checkShow();
};
/**
* Set to true whenever a field thinks it's output is important
*/
FocusManager.prototype.setImportantFieldFlag = function(flag) {
if (this.debug) {
console.log('FocusManager.setImportantFieldFlag', flag);
}
this._importantFieldFlag = flag;
this._checkShow();
};
/**
* Set to true whenever a field thinks it's output is important
*/
FocusManager.prototype.setError = function(isError) {
if (this.debug) {
console.log('FocusManager._isError', isError);
}
this._isError = isError;
this._checkShow();
};
/**
* Helper to compare the current showing state with the value calculated by
* _shouldShow() and take appropriate action
*/
FocusManager.prototype._checkShow = function() {
var fire = false;
var ev = {
tooltipVisible: this.isTooltipVisible,
outputVisible: this.isOutputVisible
};
var showTooltip = this._shouldShowTooltip();
if (this.isTooltipVisible !== showTooltip.visible) {
ev.tooltipVisible = this.isTooltipVisible = showTooltip.visible;
fire = true;
}
var showOutput = this._shouldShowOutput();
if (this.isOutputVisible !== showOutput.visible) {
ev.outputVisible = this.isOutputVisible = showOutput.visible;
fire = true;
}
if (fire) {
if (this.debug) {
console.log('FocusManager.onVisibilityChange', ev);
}
this.onVisibilityChange(ev);
}
};
/**
* Calculate if we should be showing or hidden taking into account all the
* available inputs
*/
FocusManager.prototype._shouldShowTooltip = function() {
var eagerHelper = this.settings.get('eagerHelper');
if (eagerHelper.value === Eagerness.NEVER) {
return { visible: false, reason: 'eagerHelperNever' };
}
if (eagerHelper.value === Eagerness.ALWAYS) {
return { visible: true, reason: 'eagerHelperAlways' };
}
if (!this._hasFocus) {
return { visible: false, reason: 'notHasFocus' };
}
if (this._isError) {
return { visible: true, reason: 'isError' };
}
if (this._helpRequested) {
return { visible: true, reason: 'helpRequested' };
}
if (this._importantFieldFlag) {
return { visible: true, reason: 'importantFieldFlag' };
}
return { visible: false, reason: 'default' };
};
/**
* Calculate if we should be showing or hidden taking into account all the
* available inputs
*/
FocusManager.prototype._shouldShowOutput = function() {
if (!this._hasFocus) {
return { visible: false, reason: 'notHasFocus' };
}
if (this._recentOutput) {
return { visible: true, reason: 'recentOutput' };
}
return { visible: false, reason: 'default' };
};
exports.FocusManager = FocusManager;