Share Gaia assets and adapters, keep private profiles and development documents local, and update both public READMEs.
485 lines
16 KiB
JavaScript
485 lines
16 KiB
JavaScript
'use strict';
|
|
/* global Event */
|
|
/* global ScreenLayout */
|
|
/* global SettingsListener */
|
|
/* global Service */
|
|
|
|
(function(exports) {
|
|
|
|
/**
|
|
* SoftwareButtonManager manages a home button for devices without
|
|
* physical home buttons. The software home button will display at the bottom
|
|
* of the screen in portrait, and on the right in landscape and is meant to
|
|
* function in the same way as a hardware home button.
|
|
* @class SoftwareButtonManager
|
|
* @requires ScreenLayout
|
|
* @requires SettingsListener
|
|
*/
|
|
function SoftwareButtonManager() {
|
|
this.isMobile = ScreenLayout.getCurrentLayout('tiny');
|
|
this.isOnRealDevice = ScreenLayout.isOnRealDevice();
|
|
this.hasHardwareHomeButton =
|
|
ScreenLayout.getCurrentLayout('hardwareHomeButton');
|
|
this.element = document.getElementById('software-buttons');
|
|
this.fullscreenLayoutElement =
|
|
document.getElementById('software-buttons-fullscreen-layout');
|
|
this.homeButtons = [
|
|
document.getElementById('software-home-button'),
|
|
document.getElementById('fullscreen-software-home-button'),
|
|
document.getElementById('fullscreen-layout-software-home-button')
|
|
];
|
|
this.screenElement = document.getElementById('screen');
|
|
// Bind this to the tap function, if it's done in the
|
|
// addEventListener call the removeEventListener won't work properly
|
|
this._fullscreenTapFunction = this._fullscreenTapFunction.bind(this);
|
|
this.enabled = !this.hasHardwareHomeButton && this.isMobile;
|
|
// enabled is true on mobile that has no hardware home button
|
|
}
|
|
|
|
SoftwareButtonManager.prototype = {
|
|
name: 'SoftwareButtonManager',
|
|
|
|
/**
|
|
* True if the device has a hardware home button.
|
|
* @memberof SoftwareButtonManager.prototype
|
|
* @type {Boolean}
|
|
*/
|
|
hasHardwareHomeButton: true,
|
|
|
|
/**
|
|
* Whether or not the SoftwareButtonManager is enabled.
|
|
* @memberof SoftwareButtonManager.prototype
|
|
* @type {Boolean}
|
|
*/
|
|
_enabled: false,
|
|
get enabled() {
|
|
return this._enabled;
|
|
},
|
|
set enabled(value) {
|
|
var shouldDispatch = false;
|
|
if (typeof(this._enabled) !== 'undefined' &&
|
|
this._enabled !== value) {
|
|
shouldDispatch = true;
|
|
}
|
|
this._enabled = value;
|
|
if (value) {
|
|
this._currentOrientation = Service.query('fetchCurrentOrientation');
|
|
window.screen.addEventListener('mozorientationchange', this);
|
|
window.addEventListener('appwindow-orientationchange', this);
|
|
|
|
window.addEventListener('mozfullscreenchange', this);
|
|
window.addEventListener('homegesture-enabled', this);
|
|
window.addEventListener('homegesture-disabled', this);
|
|
|
|
window.addEventListener('system-resize',
|
|
this._updateButtonRect.bind(this));
|
|
window.addEventListener('edge-touch-redispatch', this);
|
|
window.addEventListener('hierachychanged', this);
|
|
} else {
|
|
window.screen.removeEventListener('mozorientationchange', this);
|
|
window.removeEventListener('appwindow-orientationchange', this);
|
|
|
|
window.removeEventListener('mozfullscreenchange', this);
|
|
window.removeEventListener('homegesture-enabled', this);
|
|
window.removeEventListener('homegesture-disabled', this);
|
|
|
|
window.removeEventListener('system-resize',
|
|
this._updateButtonRect.bind(this));
|
|
window.removeEventListener('edge-touch-redispatch', this);
|
|
window.removeEventListener('hierachychanged', this);
|
|
}
|
|
shouldDispatch && this.resizeAndDispatchEvent();
|
|
},
|
|
|
|
/**
|
|
* Enables the software button if hasHardwareHomeButton is false.
|
|
* @memberof SoftwareButtonManager.prototype
|
|
* @type {Boolean}
|
|
*/
|
|
overrideFlag: false,
|
|
|
|
/**
|
|
* Returns the height of the software buttons if device
|
|
* is in portrait, 0 otherwise.
|
|
* @memberof SoftwareButtonManager.prototype
|
|
* @return The height of the software buttons element.
|
|
*/
|
|
_cacheHeight: null,
|
|
get height() {
|
|
if (!this.enabled ||
|
|
(this._currentOrientation &&
|
|
!this._currentOrientation.includes('portrait'))) {
|
|
return 0;
|
|
}
|
|
|
|
return this._cacheHeight ||
|
|
(this._cacheHeight = this.element.getBoundingClientRect().height);
|
|
},
|
|
|
|
/**
|
|
* Returns the width of the software buttons if device
|
|
* is in landscape, 0 otherwise.
|
|
* @memberof SoftwareButtonManager.prototype
|
|
* @return The width of the software buttons element.
|
|
*/
|
|
_cacheWidth: null,
|
|
get width() {
|
|
if (!this.enabled || !this._currentOrientation.includes('landscape')) {
|
|
return 0;
|
|
}
|
|
|
|
return this._cacheWidth ||
|
|
(this._cacheWidth = this.element.getBoundingClientRect().width);
|
|
},
|
|
|
|
_buttonRect: null,
|
|
_updateButtonRect: function() {
|
|
var isFullscreen = !!document.mozFullScreenElement;
|
|
var activeApp = Service.query('getTopMostWindow');
|
|
var isFullscreenLayout = activeApp && activeApp.isFullScreenLayout();
|
|
|
|
var button;
|
|
if (isFullscreenLayout) {
|
|
button = this.homeButtons[2];
|
|
} else if (isFullscreen) {
|
|
button = this.homeButtons[1];
|
|
} else {
|
|
button = this.homeButtons[0];
|
|
}
|
|
|
|
this._buttonRect = button.getBoundingClientRect();
|
|
},
|
|
|
|
/**
|
|
* The current device orientation.
|
|
* @memberof SoftwareButtonManager.prototype
|
|
* @type {String}
|
|
*/
|
|
_currentOrientation: null,
|
|
|
|
/**
|
|
* Starts the SoftwareButtonManager instance.
|
|
* @memberof SoftwareButtonManager.prototype
|
|
*/
|
|
start: function() {
|
|
if (this.isMobile) {
|
|
if (!this.hasHardwareHomeButton && this.isOnRealDevice) {
|
|
this.overrideFlag = true;
|
|
|
|
var lock = SettingsListener.getSettingsLock();
|
|
var req = lock.get('homegesture.enabled');
|
|
req.onsuccess = function sbm_getHomeGestureEnabled() {
|
|
var gestureEnabled = req.result['homegesture.enabled'];
|
|
lock.set({'software-button.enabled': !gestureEnabled});
|
|
};
|
|
}
|
|
|
|
SettingsListener.observe('software-button.enabled', false,
|
|
function onObserve(value) {
|
|
// Default settings from build/settings.js will override the value
|
|
// of 'software-button.enabled', so we set a flag to avoid it
|
|
// in case.
|
|
if (this.overrideFlag) {
|
|
this.overrideFlag = false;
|
|
return;
|
|
}
|
|
this.enabled = value;
|
|
this.toggle();
|
|
}.bind(this));
|
|
} else {
|
|
this.enabled = false;
|
|
this.toggle();
|
|
}
|
|
Service.registerState('width', this);
|
|
Service.registerState('height', this);
|
|
Service.registerState('enabled', this);
|
|
},
|
|
|
|
/**
|
|
* Resizes software buttons panel and dispatches events so screens
|
|
* can resize themselves after a change in the state of
|
|
* the software home button.
|
|
* @memberof SoftwareButtonManager.prototype
|
|
*/
|
|
resizeAndDispatchEvent: function() {
|
|
var element = this.element;
|
|
element.classList.toggle('visible', !!(this.enabled));
|
|
var previousState = this.enabled ? 'disabled' : 'enabled';
|
|
var currentState = this.enabled ? 'enabled' : 'disabled';
|
|
this.screenElement.classList.add('software-button-' + currentState);
|
|
this.screenElement.classList.remove('software-button-' + previousState);
|
|
window.dispatchEvent(new Event('software-button-' + currentState));
|
|
},
|
|
|
|
/**
|
|
* Shortcut to publish a custom software button event.
|
|
* @memberof SoftwareButtonManager.prototype
|
|
* @param {String} type The type of softwareButtonEvent.
|
|
*/
|
|
publish: function(type) {
|
|
this.element.dispatchEvent(new CustomEvent('softwareButtonEvent', {
|
|
bubbles: true,
|
|
detail: {
|
|
type: type
|
|
}
|
|
}));
|
|
},
|
|
|
|
/**
|
|
* Toggles the status of the software button.
|
|
* @memberof SoftwareButtonManager.prototype
|
|
*/
|
|
toggle: function() {
|
|
delete this._cacheHeight;
|
|
delete this._cacheWidth;
|
|
|
|
if (this.enabled) {
|
|
this.element.addEventListener('mousedown', this._preventFocus);
|
|
this.homeButtons.forEach(function sbm_addTouchListeners(b) {
|
|
b.addEventListener('touchstart', this);
|
|
b.addEventListener('mousedown', this);
|
|
b.addEventListener('touchend', this);
|
|
}.bind(this));
|
|
window.addEventListener('mozfullscreenchange', this);
|
|
} else {
|
|
this.element.removeEventListener('mousedown', this._preventFocus);
|
|
this.homeButtons.forEach(function sbm_removeTouchListeners(b) {
|
|
b.removeEventListener('touchstart', this);
|
|
b.removeEventListener('mousedown', this);
|
|
b.removeEventListener('touchend', this);
|
|
}.bind(this));
|
|
window.removeEventListener('mozfullscreenchange', this);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* General event handler interface.
|
|
* @memberof SoftwareButtonManager.prototype
|
|
* @param {DOMEvent} evt The event.
|
|
*/
|
|
handleEvent: function(evt) {
|
|
switch (evt.type) {
|
|
case 'mousedown':
|
|
// Prevent the button from receving focus.
|
|
evt.preventDefault();
|
|
break;
|
|
case 'touchstart':
|
|
this.press();
|
|
break;
|
|
case 'touchend':
|
|
this.release();
|
|
break;
|
|
case 'edge-touch-redispatch':
|
|
this.handleRedispatchedTouch(evt);
|
|
break;
|
|
case 'homegesture-disabled':
|
|
// at least one of software home button or gesture is enabled
|
|
// when no hardware home button
|
|
if (!this.hasHardwareHomeButton && !this.enabled) {
|
|
SettingsListener.getSettingsLock()
|
|
.set({'software-button.enabled': true});
|
|
}
|
|
break;
|
|
case 'homegesture-enabled':
|
|
if (this.enabled) {
|
|
SettingsListener.getSettingsLock()
|
|
.set({'software-button.enabled': false});
|
|
}
|
|
break;
|
|
case 'mozfullscreenchange':
|
|
if (!this.enabled) {
|
|
return;
|
|
}
|
|
|
|
window.clearTimeout(this._fullscreenTimerId);
|
|
this._fullscreenTimerId = 0;
|
|
|
|
if (document.mozFullScreenElement) {
|
|
this.fullscreenLayoutElement.classList.add('hidden');
|
|
|
|
this._fullscreenElement = document.mozFullScreenElement;
|
|
this._fullscreenElement
|
|
.addEventListener('touchstart', this._fullscreenTapFunction);
|
|
this._fullscreenElement
|
|
.addEventListener('touchend', this._fullscreenTapFunction);
|
|
} else if (this._fullscreenElement) {
|
|
this.fullscreenLayoutElement.classList.remove('hidden');
|
|
|
|
this._fullscreenElement
|
|
.removeEventListener('touchstart', this._fullscreenTapFunction);
|
|
this._fullscreenElement
|
|
.removeEventListener('touchend', this._fullscreenTapFunction);
|
|
this._fullscreenElement = null;
|
|
}
|
|
|
|
this._updateButtonRect();
|
|
break;
|
|
case 'mozorientationchange':
|
|
// mozorientationchange is fired before 'system-resize'
|
|
// so we can adjust width/height before that happens.
|
|
var isPortrait = this._currentOrientation.contains('portrait');
|
|
var newOrientation = Service.query('fetchCurrentOrientation');
|
|
if (isPortrait && newOrientation.contains('landscape')) {
|
|
this.element.style.right = this.element.style.bottom;
|
|
this.element.style.bottom = null;
|
|
} else if (!isPortrait && newOrientation.includes('portrait')) {
|
|
this.element.style.bottom = this.element.style.right;
|
|
this.element.style.right = null;
|
|
}
|
|
this._currentOrientation = newOrientation;
|
|
|
|
// The mozorientationchange happens before redraw and orientation
|
|
// change after, so this is done to avoid animation of the soft button
|
|
this.element.classList.add('no-transition');
|
|
break;
|
|
case 'appwindow-orientationchange':
|
|
this.element.classList.remove('no-transition');
|
|
break;
|
|
case 'hierachychanged':
|
|
if (this.enabled && Service.query('getTopMostWindow')) {
|
|
this.element.classList.toggle('attention-lockscreen',
|
|
Service.query('getTopMostWindow').CLASS_NAME ===
|
|
'LockScreenWindow');
|
|
}
|
|
break;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Used to prevent taps on the SHB container from stealing focus, and to
|
|
* prevent fuzzing issues where tapping will trigger events in the app.
|
|
* @memberof SoftwareButtonManager.prototype
|
|
*/
|
|
_preventFocus: function(evt) {
|
|
evt.preventDefault();
|
|
},
|
|
|
|
/**
|
|
* The id of the timer that hides the soft buttons in fullscreen.
|
|
* Saved so that the timer can be canceled if the user clicks/taps
|
|
* the screen again.
|
|
* @memberof SoftwareButtonManager.prototype
|
|
* @type {number}
|
|
*/
|
|
_fullscreenTimerId: 0,
|
|
|
|
/**
|
|
* The element that entered fullscreen.
|
|
* Saved so that click eventListener can be removed when leaving fullscreen.
|
|
* @memberof SoftwareButtonManager.prototype
|
|
* @type {DomElement}
|
|
*/
|
|
_fullscreenElement: null,
|
|
|
|
/**
|
|
* The starting position of a touch in fullscreen.
|
|
* Saved so that we can check whether the user taps or swipes.
|
|
* @memberof SoftwareButtonManager.prototype
|
|
* @type {Touch}
|
|
*/
|
|
_fullscreenTouchStart: null,
|
|
|
|
/**
|
|
* Function to execute when user clicks/taps screen in fullscreen mode.
|
|
* @memberof SoftwareButtonManager.prototype
|
|
*/
|
|
_fullscreenTapFunction: function (evt) {
|
|
switch (evt.type) {
|
|
case 'touchstart':
|
|
this._fullscreenTouchStart = evt.touches[0];
|
|
break;
|
|
case 'touchend':
|
|
var touch = evt.changedTouches[0];
|
|
var xDistance =
|
|
Math.abs(touch.pageX - this._fullscreenTouchStart.pageX);
|
|
var yDistance =
|
|
Math.abs(touch.pageY - this._fullscreenTouchStart.pageY);
|
|
|
|
var swipeThreshold = 10;
|
|
if (xDistance < swipeThreshold && yDistance < swipeThreshold) {
|
|
window.clearTimeout(this._fullscreenTimerId);
|
|
|
|
if (this.fullscreenLayoutElement.classList.contains('hidden')) {
|
|
this.fullscreenLayoutElement.classList.remove('hidden');
|
|
this._fullscreenTimerId =
|
|
window.setTimeout(function sbm_fullscreenHideTimer() {
|
|
this.fullscreenLayoutElement.classList.add('hidden');
|
|
}.bind(this), 3000);
|
|
} else {
|
|
// We wait for a bit to get a chance to process a potential
|
|
// mozfullscreenchange
|
|
this._fullscreenTimerId =
|
|
window.setTimeout(function sbm_fullscreenHideTimer() {
|
|
this.fullscreenLayoutElement.classList.add('hidden');
|
|
}.bind(this), 100);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
},
|
|
|
|
press: function() {
|
|
this.homeButtons.forEach(function sbm_addActive(b) {
|
|
b.classList.add('active');
|
|
});
|
|
|
|
this.publish('home-button-press');
|
|
},
|
|
|
|
release: function() {
|
|
this.homeButtons.forEach(function sbm_removeActive(b) {
|
|
b.classList.remove('active');
|
|
});
|
|
|
|
this.publish('home-button-release');
|
|
},
|
|
|
|
_pressedByRedispatch: false,
|
|
handleRedispatchedTouch: function(evt) {
|
|
var type = evt.detail.type;
|
|
|
|
if (!this._onButton(evt)) {
|
|
if (type !== 'touchstart' && this._pressedByRedispatch) {
|
|
this._pressedByRedispatch = false;
|
|
this.release();
|
|
}
|
|
return;
|
|
}
|
|
|
|
evt.preventDefault();
|
|
|
|
switch (type) {
|
|
case 'touchstart':
|
|
this.press();
|
|
this._pressedByRedispatch = true;
|
|
break;
|
|
case 'touchend':
|
|
this._pressedByRedispatch = false;
|
|
this.release();
|
|
break;
|
|
}
|
|
},
|
|
|
|
_onButton: function(e) {
|
|
var type = e.detail.type;
|
|
var touch = (type === 'touchend') ?
|
|
e.detail.changedTouches[0] : e.detail.touches[0];
|
|
|
|
var x = touch.pageX;
|
|
var y = touch.pageY;
|
|
|
|
var radius = 4;
|
|
var rect = this._buttonRect;
|
|
var leftBound = rect.left - radius;
|
|
var rightBound = rect.right + radius;
|
|
var topBound = rect.top - radius;
|
|
var bottomBound = rect.bottom + radius;
|
|
|
|
return (x >= leftBound && x <= rightBound &&
|
|
y >= topBound && y <= bottomBound);
|
|
}
|
|
};
|
|
|
|
exports.SoftwareButtonManager = SoftwareButtonManager;
|
|
|
|
}(window));
|