/* global AppChrome */
/* global AudioChannelController */
/* global BrowserFrame */
/* global layoutManager */
/* global ManifestHelper */
/* global OrientationManager */
/* global ScreenLayout */
/* global SettingsCache */
/* global Service */
/* global focusManager */
'use strict';
(function(exports) {
// Turn on this flag to debug all windows.
var DEBUG = false;
// Turn on this flag to print all trace in debugging function.
var TRACE = false;
var _id = 0;
/**
* AppWindow creates, contains, manages a
* [mozbrowser](https://developer.mozilla.org/en-US/docs/WebAPI/Browser)
* iframe. AppWindow is directly managed by AppWindowManager,
* by call resize(), open(), close() on AppWindow.
*
* Basically AppWindow would manipulate all mozbrowser events
* fired from the mozbrowser iframe by itself and show relevant UI.
*
* AppWindow is also the parent class of ActivityWindow and HomescreenWindow.
* Mostly they do the same thing but is different at some points
* like the way transitioning.
*
* About creating an AppWindow,
* you need to provide at least the web app/page URL.
* If you have also provided the manifest,
* then you would get an AppWindow object which is a web app.
* Otherwise you would get an AppWindow which is in 'Wrapper' type.
* The only one different thing between web app and web page is
* just the manifestURL.
*
* ##### Life cycle state machine of an appWindow instance
* 
*
* @example
* var app = new AppWindow({
* url: 'http://uitest.gaiamobile.org:8080/index.html',
* manifestURL: 'http://uitest.gaiamobile.org:8080/manifest.webapp'
* });
* app.open();
*
* @class AppWindow
* @mixes BrowserMixin into AppWindow.prototype
*/
var AppWindow = function AppWindow(configuration) {
this.reConfig(configuration);
this.render();
/**
* This is fired when the app window is instantiated.
* @event AppWindow#appcreated
*/
this.publish('created');
if (DEBUG || this._DEBUG) {
this.constructor[this.instanceID] = this;
}
this.launchTime = Date.now();
focusManager.addUI(this);
return this;
};
/**
* When this option is set to true,
* an app would not be removed while it's crashed.
* And while it's opened next, we will re-render the mozbrowser iframe.
* @type {Boolean}
*/
AppWindow.SUSPENDING_ENABLED = false;
SettingsCache.observe('app-suspending.enabled', false, function(value) {
AppWindow.SUSPENDING_ENABLED = !!value;
});
/**
* Change this if new window has its own styles.
*
* @type string
* @memberof AppWindow
*/
AppWindow.prototype.CLASS_LIST = 'appWindow';
AppWindow.prototype._DEBUG = false;
/**
* Generate instanceID of this instance.
*/
AppWindow.prototype.generateID = function() {
if (!this.instanceID) {
this.instanceID = this.CLASS_NAME + '_' + _id;
_id++;
}
};
/**
* Generate all configurations we need.
* @param {Object} configuration Initial configuration object
* Includes manifestURL, manifest, url, origin, name.
*/
AppWindow.prototype.reConfig = function aw_reConfig(configuration) {
// Some modules are querying appWindow.manifestURL or appWindow.origin
// so we inject all configurations into appWindow first.
for (var key in configuration) {
this[key] = configuration[key];
}
this.browser_config = configuration;
// Store initial configuration in this.config
this.config = configuration;
this.config.chrome = (this.manifest && this.manifest.chrome) ?
this.manifest.chrome :
this.config.chrome;
if (!this.config.chrome) {
this.config.chrome = {
scrollable: this.isBrowser(),
maximized: this.isBrowser()
};
} else if (this.config.chrome.navigation) {
this.config.chrome.scrollable = !this.isFullScreen();
// This is for backward compatibility with application that
// requests the |navigation| flag in their manifest.
this.config.chrome.maximized = true;
}
if (!this.manifest && this.config && this.config.title) {
this.updateName(this.config.title);
} else {
this.name = new ManifestHelper(this.manifest).name;
}
// Get icon splash
this.getIconForSplash();
this.generateID();
/**
* The instanceID of the root window of this window.
* @type {String}
*/
this.groupID = this.getRootWindow().instanceID;
if (this.previousWindow) {
this.previousWindow.setNextWindow(this);
} else if (this.rearWindow) {
this.rearWindow.setFrontWindow(this);
}
};
/**
* Update the name of this window.
* @param {String} name The new name.
*/
AppWindow.prototype.updateName = function aw_updateName(name) {
if (this.config && this.config.title) {
this.config.title = name;
}
this.name = name;
};
/**
* Represent the current pagee visibility state,
* i.e. what is currently visible. Possible value:
* 'foreground': setVisible(true)
* 'background': setVisible(false)
*
* Default value is foreground.
*/
AppWindow.prototype._visibilityState = 'foreground';
/**
* The rotating degree of current frame.
*/
AppWindow.prototype.rotatingDegree = 0;
AppWindow.prototype._dump = function aw__dump() {
console.log('======================');
try {
throw new Error('e');
} catch (e) {
this.debug(e.stack);
}
console.log('======================');
};
/**
* In order to prevent flashing of unpainted frame
* during switching from one to another,
* many event listener & callbacks are employed.
*
* When 'cardviewbeforeshow' is received, the screenshotOverlay is shown.
*
* When 'cardviewclosed' is received, the screenshotOverlay is hidden.
*
* When 'sheetsgesturebegin' is received, the screenshotOverlay is shown.
*
* When 'sheetsgestureend' is received the screenshotOverlay is hidden.
*
* However, setVisible *never* controls the visibility of the
* screenshotOverlay. The visibility of it is entirely event driven or can
* be triggered manually be calling _showScreenshotOverlay and
* _hideScreenshotOverlay.
*/
AppWindow.prototype.setVisible =
function aw_setVisible(visible) {
this.debug('set visibility -> ', visible);
this.setVisibleForScreenReader(visible);
if (visible) {
// If this window is not the lockscreen, and the screen is locked,
// we need to aria-hide the window.
this._showFrame();
} else {
this._hideFrame();
}
if (this.frontWindow) {
this.frontWindow.setVisible(visible);
}
};
/**
* Set screen reader visibility.
* @type {Boolean} A flag indicating if it should be visible to the screen
* reader.
*/
AppWindow.prototype.setVisibleForScreenReader =
function aw_setVisibleForScreenReader(visible) {
this.element.setAttribute('aria-hidden', !visible);
};
/**
* _showFrame turn on the frame visibility.
* So this shouldn't be invoked by others directly.
*/
AppWindow.prototype._showFrame = function aw__showFrame() {
this.debug('before showing frame');
// If we're already showing, do nothing!
if (!this.browser.element.classList.contains('hidden')) {
return;
}
this.browser.element.classList.remove('hidden');
this._setVisible(true);
if (this.isHomescreen) {
return;
}
// Getting a new screenshot to force compositing before
// removing the screenshot overlay if it exists.
if (this.screenshotOverlay &&
this.screenshotOverlay.classList.contains('visible')) {
this.tryWaitForFullRepaint(this._hideScreenshotOverlay.bind(this));
}
};
/**
* _hideFrame will turn off the frame visibility.
* So this shouldn't be invoked by others directly.
*/
AppWindow.prototype._hideFrame = function aw__hideFrame() {
this.debug('before hiding frame');
// If we're already hidden, we have nothing to do!
if (this.browser.element.classList.contains('hidden')) {
return;
}
this._setVisible(false);
this.browser.element.classList.add('hidden');
};
/**
* An appWindow is active means:
* 1. Going to be opened.
* 2. Already opened.
*
* Note: The element has active class unless it's at closed state.
* But a closing instance is not recognized as active.
*
* @return {Boolean} The instance is active or not.
*/
AppWindow.prototype.isActive = function aw_isActive() {
if (!this.element) {
return false;
}
if (this.element.classList.contains('will-become-active')) {
return true;
}
if (this.transitionController) {
return (this.transitionController._transitionState == 'opened' ||
this.transitionController._transitionState == 'opening');
} else {
// Before the transition controller is inited
return false;
}
};
/**
* TODO: Integrate swipe transition.
*
* @return {Boolean} The instance is transitioning or not.
*/
AppWindow.prototype.isTransitioning = function aw_isTransitioning() {
if (this.transitionController) {
return (this.transitionController._transitionState == 'opening' ||
this.transitionController._transitionState == 'closing');
} else {
// Fallback
return (this.element.classList.contains('transition-opening') ||
this.element.classList.contains('transition-closing'));
}
};
/**
* If the instance is crashed but not resumed yet,
* it would be in suspended state.
*
* The state would be turned off once we render the browser again.
* @type {Boolean}
*/
AppWindow.prototype.suspended = false;
/**
* Re-render the browser element with the same config.
*/
AppWindow.prototype.reviveBrowser = function() {
if (this.browser) {
return;
}
this.debug(' ...revived!');
this.browser = new BrowserFrame(this.browser_config);
this.browserContainer.appendChild(this.browser.element);
this.iframe = this.browser.element;
this.launchTime = Date.now();
this.suspended = false;
this.element.classList.remove('suspended');
// Launch as background by default.
this._setVisible(false);
this.publish('resumed');
};
/**
* Remove the browser element and clear the states of the browser.
* @fires AppWindow#suspended
*/
AppWindow.prototype.destroyBrowser = function() {
if (!this.browser) {
return;
}
this.loading = false;
this.loaded = false;
this.suspended = true;
this.element.classList.add('suspended');
this.browserContainer.removeChild(this.browser.element);
this.browser = null;
this.publish('suspended');
};
/**
* Kill an instance.
*
* 1. If this instance has any living activity window as callee,
* all of the activity window callee would be killed.
* 2. If this instance is active, it would request to close.
*
* @fires AppWindow#appterminated
*/
AppWindow.prototype.kill = function aw_kill(evt) {
if (this._killed) {
return;
}
if (!this.isHomescreen) {
this._killed = true;
}
if (DEBUG || this._DEBUG) {
this.constructor[this.instanceID] = null;
}
// Remove callee <-> caller reference before we remove the window.
if (this.callerWindow) {
this.callerWindow.calleeWindow = null;
this.callerWindow = null;
}
if (this.calleeWindow) {
this.calleeWindow.callerWindow = null;
this.calleeWindow = null;
}
// Kill any front window.
if (this.frontWindow) {
this.frontWindow.kill();
this.frontWindow = null;
}
// Kill any next window.
if (this.nextWindow) {
this.nextWindow.kill();
this.nextWindow = null;
}
// Kill any attention window.
if (this.attentionWindow) {
this.attentionWindow.kill();
this.attentionWindow = null;
}
// Remove registered global events
this.constructor.REGISTERED_GLOBAL_EVENTS.forEach(function iterator(evt) {
this.debug('removing ' + evt + ' global event handler ...');
window.removeEventListener(evt, this);
}, this);
// If the app is the currently displayed app, switch to the homescreen
if (this.isActive() && !this.isHomescreen) {
var fallbackTimeout;
var onClosed = function() {
clearTimeout(fallbackTimeout);
this.element.removeEventListener('_closed', onClosed);
this.destroy();
this.publish('terminated');
}.bind(this);
this.element.addEventListener('_closed', onClosed);
fallbackTimeout = setTimeout(onClosed,
this.transitionController.CLOSING_TRANSITION_TIMEOUT);
if (this.previousWindow) {
this.previousWindow.getBottomMostWindow().open('in-from-left');
this.close('out-to-right');
} else {
this.requestClose();
}
} else {
this.destroy();
/**
* Fired when the instance is terminated.
* @event AppWindow#appterminated
*/
this.publish('terminated');
}
};
/**
* An appWindow is dead if somebody requested it to be killed.
*
* @return {Boolean} The instance is dead or not.
*/
AppWindow.prototype.isDead = function aw_isDead() {
return (this._killed || !this.element);
};
/**
* Destroy the instance.
* @fires AppWindow#appdestroyed
*/
AppWindow.prototype.destroy = function aw_destroy() {
/**
* Fired before the instance id destroyed.
* @event AppWindow#appwilldestroy
*/
// Remove previous -> next reference.
if (this.previousWindow) {
this.previousWindow.unsetNextWindow();
this.previousWindow = null;
}
// Remove rear -> front reference.
if (this.rearWindow) {
this.rearWindow.unsetFrontWindow();
this.rearWindow = null;
}
// Remove attention reference.
if (this.parentWindow) {
this.parentWindow.unsetAttentionWindow();
this.parentWindow = null;
}
this.publish('willdestroy');
this.uninstallSubComponents();
focusManager.removeUI(this);
if (this.element) {
if (this.element.parentNode) {
this.element.parentNode.removeChild(this.element);
}
this.element = null;
}
/**
* Fired after the instance id destroyed.
* @event AppWindow#appdestroyed
*/
this.publish('destroyed');
};
AppWindow.prototype.containerElement = document.getElementById('windows');
AppWindow.prototype.view = function aw_view() {
return '
' +
'
' +
'
' +
'
' +
'
' +
'
' +
'
';
};
/**
* Render the mozbrowser iframe and some overlays.
* @inner
*/
AppWindow.prototype._render = function aw__render() {
if (this.element) {
return;
}
/**
* Fired before this element is appended to the DOM tree.
* @event AppWindow#appwillrender
*/
this.publish('willrender');
var range = document.createRange();
var fragment = range.createContextualFragment(this.view());
// window.open would offer the iframe so we don't need to generate.
if (this.iframe) {
this.browser = {
element: this.iframe
};
} else {
this.browser = new BrowserFrame(this.browser_config);
}
this.element = fragment.getElementById(this.instanceID);
// For gaiauitest usage.
this.element.dataset.manifestName = this.manifest ? this.manifest.name : '';
// XXX: Remove following two lines once mozbrowser element is moved
// into appWindow.
this.frame = this.element;
this.iframe = this.browser.element;
this.iframe.dataset.frameType = 'window';
this.iframe.dataset.frameOrigin = this.origin;
this.iframe.dataset.url = this.config.url;
if (this.isFullScreen()) {
this.element.classList.add('fullscreen-app');
}
if (this.isBrowser()) {
this.element.classList.add('browser');
}
this.browserContainer = this.element.querySelector('.browser-container');
this.browserContainer.appendChild(this.browser.element);
this.containerElement.appendChild(fragment);
this.screenshotOverlay = this.element.querySelector('.screenshot-overlay');
this.fadeOverlay = this.element.querySelector('.fade-overlay');
var overlay = '.identification-overlay';
this.identificationOverlay = this.element.querySelector(overlay);
var icon = '.identification-overlay .icon';
this.identificationIcon = this.element.querySelector(icon);
var title = '.identification-overlay .title';
this.identificationTitle = this.element.querySelector(title);
// Launched as background: set visibility and overlay screenshot.
// homescreen is launched at background under FTU/lockscreen too.
if (this.config.stayBackground || this.isHomescreen) {
this.setVisible(false);
}
/**
* Fired after the app window element is appended to the DOM tree.
* @event AppWindow#apprendered
*/
this.publish('rendered');
this._rendered = true;
setTimeout(this.setFrameBackground.bind(this));
};
AppWindow.prototype.render = function aw_render() {
this._render();
this._registerEvents();
this.installSubComponents();
// Pre determine the rotation degree.
this.determineRotationDegree();
};
/**
* Check an appWindow is a regular browsing window, not tied to a particular
* application.
*
* @return {Boolean} is the current instance a browsing window.
*/
AppWindow.prototype.isBrowser = function aw_isbrowser() {
return !this.manifestURL;
};
/**
* Check an appWindow contains a certified application
*
* @return {Boolean} is the current instance a certified application.
*/
AppWindow.prototype.isCertified = function aw_iscertified() {
return this.config.manifest && 'certified' === this.config.manifest.type;
};
/**
* Try to navigate the current frame to a given url. If the current instance
* is not a browsing window, we reconfigure it to be a browser window, and
* give it a clean browser frame.
* @param {String} url The url to navigate to
*/
AppWindow.prototype.navigate = function aw_isbrowser(url) {
// Bug 1071882 - We currently only support navigating browser windows and
// apps with a role="search", which we use for the browser landing page.
var appIsSearch = this.manifest && this.manifest.role === 'search';
if (!this.isBrowser() && !appIsSearch) {
console.warn('Tried to navigate an illegal app window.');
return;
}
// Kill any front window.
if (this.frontWindow) {
this.frontWindow.kill();
this.frontWindow = null;
}
if (!this.isBrowser()) {
// Handle navigating from an app -> browser.
this.manifestURL = null;
this.manifest = null;
this.element.classList.add('browser');
// Reset the browser
this.reConfig({
url: url,
title: url,
oop: true
});
this.browserContainer.removeChild(this.browser.element);
this.browser = new BrowserFrame(this.browser_config);
this.browserContainer.appendChild(this.browser.element);
this.iframe = this.browser.element;
this.launchTime = Date.now();
this.appChrome && this.appChrome.reConfig();
// Register audio channels for new browser frame.
this._unregisterAudioChannels();
this._registerAudioChannels();
}
this.browser.element.src = url;
};
/**
* The event is necessary for edge gesture swiper.
* It means the app window is swiped in.
*
* @access private
* @event AppWindow~_swipein
*/
/**
* The event is necessary for edge gesture swiper.
* It means the app window is swiped out.
*
* @access private
* @event AppWindow~_swipeout
*/
AppWindow.REGISTERED_EVENTS =
['mozbrowserclose', 'mozbrowsererror', 'mozbrowservisibilitychange',
'mozbrowserloadend', 'mozbrowseractivitydone', 'mozbrowserloadstart',
'mozbrowsertitlechange', 'mozbrowserlocationchange',
'mozbrowsermetachange', 'mozbrowsericonchange',
'_localized', '_swipein', '_swipeout', '_kill_suspended',
'_orientationchange', '_focus', '_hidewindow', '_sheetsgesturebegin',
'_sheetsgestureend', '_cardviewbeforeshow', '_cardviewclosed',
'_closed'];
AppWindow.REGISTERED_GLOBAL_EVENTS =
['back'];
AppWindow.SUB_COMPONENTS = {
'transitionController': window.AppTransitionController,
'modalDialog': window.AppModalDialog,
'valueSelector': window.ValueSelector,
'authDialog': window.AppAuthenticationDialog,
'contextmenu': window.BrowserContextMenu,
'childWindowFactory': window.ChildWindowFactory,
};
AppWindow.prototype.openAnimation = 'invoked';
AppWindow.prototype.closeAnimation = 'fade-out';
/**
* Install sub components belong to this window instance.
* The necessary components are based on AppWindow.SUB_COMPONENTS,
* now they are AppModalDialog/AppAuthenticationDialog.
*
* @example
* // Implement a new dialog
* var myDialog = function myDialog(app) {
* };
*
* var appWindow = new AppWindow();
* AppWindow.SUB_COMPONENTS.push(myDialog);
*
* app.installSubComponents();
*/
AppWindow.prototype.installSubComponents =
function aw_installSubComponents() {
this.debug('installing sub components...');
for (var componentName in this.constructor.SUB_COMPONENTS) {
if (this.constructor.SUB_COMPONENTS[componentName]) {
this[componentName] =
new this.constructor.SUB_COMPONENTS[componentName](this);
this[componentName].start && this[componentName].start();
}
}
// Need to wait for mozbrowserloadstart to get allowedAudioChannels.
this.browser.element.addEventListener(
'mozbrowserloadstart',
function onloadstart() {
this.browser.element.removeEventListener(
'mozbrowserloadstart', onloadstart
);
this._registerAudioChannels();
}.bind(this)
);
if (this.manifest) {
var that = this;
that.element.addEventListener('_opened', function onOpened() {
that.element.removeEventListener('_opened', onOpened);
that.appChrome = new AppChrome(that);
});
} else {
this.appChrome = new AppChrome(this);
}
};
AppWindow.prototype.uninstallSubComponents =
function aw_uninstallSubComponents() {
for (var componentName in this.constructor.SUB_COMPONENTS) {
if (this[componentName] && this[componentName].destroy) {
this[componentName].destroy();
}
this[componentName] = null;
}
this._unregisterAudioChannels();
if (this.appChrome) {
this.appChrome.destroy();
this.appChrome = null;
}
};
AppWindow.prototype._registerAudioChannels =
function aw_registerAudioChannels() {
this.audioChannels = new Map();
var audioChannels = this.browser.element.allowedAudioChannels;
audioChannels && audioChannels.forEach((audioChannel) => {
this.audioChannels.set(
audioChannel.name,
new AudioChannelController(this, audioChannel)
);
this.debug('Registered ' + audioChannel.name + ' audio channel');
});
};
AppWindow.prototype._unregisterAudioChannels =
function aw_unregisterAudioChannels() {
if (this.audioChannels) {
this.audioChannels.forEach(function(audioChannel) {
audioChannel.destroy();
});
this.audioChannels = null;
}
};
AppWindow.prototype._handle__localized = function aw__handle__localized() {
if (!this.manifest) {
return;
}
this.name = new ManifestHelper(this.manifest).name;
if (this.identificationTitle) {
this.identificationTitle.textContent = this.name;
}
// For uitest.
this.element.dataset.localizedName = this.name;
this.publish('namechanged');
};
AppWindow.prototype._handle__orientationchange = function() {
if (this.isActive()) {
// Will be resized by the AppWindowManager
return;
}
// Resize only the overlays not the app
var width = layoutManager.width;
var height = layoutManager.height;
this.iframe.style.width = this.width + 'px';
this.iframe.style.height = this.height + 'px';
this.element.style.width = width + 'px';
this.element.style.height = height + 'px';
// The homescreen doesn't have an identification overlay
if (this.isHomescreen) {
return;
}
// If the screenshot doesn't match the new orientation hide it
if (this.width != width) {
this.screenshotOverlay.style.visibility = 'hidden';
} else {
this.screenshotOverlay.style.visibility = '';
}
};
AppWindow.prototype._handle_mozbrowservisibilitychange =
function aw__handle_mozbrowservisibilitychange(evt) {
var type = evt.detail.visible ? 'foreground' : 'background';
this.publish(type);
};
AppWindow.prototype._handle_mozbrowseractivitydone =
function aw__handle_mozbrowseractivitydone(evt) {
// In case we're not ActivityWindow but we're launched
// as window disposition activity.
if (this.isActive() && this.callerWindow) {
var caller = this.callerWindow;
var callerBottom = caller.getBottomMostWindow();
var calleeBottom = this.getBottomMostWindow();
caller.calleeWindow = null;
this.callerWindow = null;
// No transition when the callee is caller
if (callerBottom !== calleeBottom) {
callerBottom.open('in-from-left');
calleeBottom.close('out-to-right');
}
}
};
AppWindow.prototype._handle_mozbrowserclose =
function aw__handle_mozbrowserclose(evt) {
this.kill();
};
AppWindow.prototype._handle_mozbrowsererror =
function aw__handle_mozbrowsererror(evt) {
if (evt.detail.type !== 'fatal') {
return;
}
// Send event instead of call crash reporter directly.
this.publish('crashed');
if (this.constructor.SUSPENDING_ENABLED && !this.isActive()) {
this.debug(' ..sleep! I will come back.');
this.destroyBrowser();
if (this.frontWindow) {
this.frontWindow.kill();
}
} else {
this.kill(evt);
}
};
AppWindow.prototype._handle_mozbrowserloadstart =
function aw__handle_mozbrowserloadstart(evt) {
this.loading = true;
this._changeState('loading', true);
this.publish('loading');
};
AppWindow.prototype._handle_mozbrowsertitlechange =
function aw__handle_handle_mozbrowsertitlechange(evt) {
this.title = evt.detail;
this.publish('titlechange');
if (this.identificationTitle && !this.manifest) {
this.identificationTitle.textContent = this.title;
}
};
AppWindow.prototype._handle_mozbrowserloadend =
function aw__handle_mozbrowserloadend(evt) {
if (!this.loaded) {
// Perf test needs.
this.publish('loadtime', {
time: parseInt(Date.now() - this.launchTime),
timestamp: this.timestamp,
type: 'c',
src: this.config.url
});
}
this.loading = false;
this.loaded = true;
this.element.classList.add('render');
// Bug 1043408 - Marionette tests relies on the render class of the
// iframe parent in order to starts. So let's replicate the 'render'
// class on browser-container until the proper patch on the external
// repo.
this.browserContainer.classList.add('render');
// Force removing background image.
this.element.style.backgroundImage = 'none';
this._changeState('loading', false);
this.publish('loaded');
var backgroundColor = evt.detail.backgroundColor;
this.debug('bgcolor= ', backgroundColor);
/* When rotating the screen, the child may take some time to reflow.
* If the child takes longer than layers.orientation.sync.timeout
* to respond, gecko will go ahead and draw anyways. This code
* uses a simple heuristic to guess the least distracting color
* we should draw in the blank space. */
/* Only allow opaque colors */
// TODOEVME - this kept throwing errors when homescreen launched,
// bgcolor was null
if (backgroundColor && backgroundColor.indexOf('rgb(') != -1 &&
!this.isHomescreen) {
this.debug('setting background color..');
this.browser.element.style.backgroundColor = backgroundColor;
}
/**
* If the window is active, we should focus it when it loaded. We didn't
* focus it when this window was opened and defered to here.
**/
if (this.getBottomMostWindow().isActive() && this.isActive()) {
focusManager.focus();
}
};
AppWindow.prototype._handle_mozbrowserlocationchange =
function aw__handle_mozbrowserlocationchange(evt) {
this.favicons = {};
this.config.url = evt.detail;
// Integration test needs to locate the frame by this attribute.
this.browser.element.dataset.url = evt.detail;
this.publish('locationchange');
};
AppWindow.prototype._handle_mozbrowsericonchange =
function aw__handle_mozbrowsericonchange(evt) {
var href = evt.detail.href;
var sizes = evt.detail.sizes;
if (!('favicons' in this)) {
this.favicons = {};
}
if (!(href in this.favicons)) {
this.favicons[href] = {sizes: []};
}
if (sizes && this.favicons[href].sizes.indexOf(sizes) === -1) {
this.favicons[href].sizes.push(sizes);
}
if (this.identificationIcon) {
this.identificationIcon.style.backgroundImage =
'url("' + evt.detail.href + '")';
}
this.publish('iconchange');
};
AppWindow.prototype._handle_mozbrowsermetachange =
function aw__handle_mozbrowsermetachange(evt) {
var detail = evt.detail;
switch (detail.name) {
case 'theme-color':
if (!detail.type) {
return;
}
// If the theme-color meta is removed, let's reset the color.
var color = '';
// Otherwise, set it to the color that has been asked.
if (detail.type !== 'removed') {
color = detail.content;
}
this.themeColor = color;
this.publish('themecolorchange');
break;
case 'application-name':
// Apps have a compulsory name field in their manifest
// which takes precedence.
if (!this.isBrowser()) {
return;
}
this.updateName(detail.content);
this.publish('namechanged');
break;
}
};
AppWindow.prototype._handle_back = function aw__handle_back(evt) {
if (document.activeElement !== this.iframe) {
return;
}
if (!this.config.url.startsWith('app://')) {
var goBackReq = this.iframe.getCanGoBack();
goBackReq.onsuccess = () => {
if (goBackReq.result) {
this.iframe.goBack();
}
};
}
};
AppWindow.prototype._registerEvents = function aw__registerEvents() {
if (this.element === null) {
this._dump();
return;
}
this.constructor.REGISTERED_EVENTS.forEach(function iterator(evt) {
this.debug('adding ' + evt + ' event handler ...');
this.element.addEventListener(evt, this);
}, this);
this.constructor.REGISTERED_GLOBAL_EVENTS.forEach(function iterator(evt) {
this.debug('adding ' + evt + ' global event handler ...');
window.addEventListener(evt, this);
}, this);
};
/**
* General event handler interface.
* Child classes shouldn't change this.
* @param {DOMEvent} evt The event.
*/
AppWindow.prototype.handleEvent = function aw_handleEvent(evt) {
// We are rendering inline activities + popup inside this element too,
// so we need to prevent ourselves to be affected
// by the mozbrowser events of the callee.
// WebAPI testing is using mozbrowserloadend event to know
// the first app is loaded so we cannot stop the propagation here.
// but we don't want subsequent mozbrowser events to bubble to the
// used-to-be-rear-window
if (this.rearWindow && evt.type.startsWith('mozbrowser')) {
evt.stopPropagation();
}
this.debug(' Handling ' + evt.type + ' event...');
if (this['_handle_' + evt.type]) {
this['_handle_' + evt.type](evt);
}
};
/**
* A temp variable to store current screenshot blob.
* We should store the blob and create objectURL
* once we need to display the image,
* and revoke right away after we finish rendering the image.
*/
AppWindow.prototype._screenshotBlob = undefined;
AppWindow.prototype.CLASS_NAME = 'AppWindow';
AppWindow.prototype.debug = function aw_debug(msg) {
if (DEBUG || this._DEBUG) {
console.log('[' + this.CLASS_NAME + ']' +
'[' + (this.name || this.origin) + ']' +
'[' + this.instanceID + ']' +
'[' + Service.currentTime() + '] ' +
Array.slice(arguments).concat());
if (TRACE) {
console.trace();
}
}
};
// Force debug output.
AppWindow.prototype.forceDebug = function aw_debug(msg) {
console.log('[Dump:' + this.CLASS_NAME + ']' +
'[' + (this.name || this.origin) + ']' +
'[' + Service.currentTime() + ']' +
Array.slice(arguments).concat());
};
AppWindow.prototype.show = function aw_show() {
if (!this.element || !this.element.classList.contains('hidden')) {
return;
}
this.element.classList.remove('hidden');
this.publish('shown');
};
AppWindow.prototype.hide = function aw_hide() {
if (!this.element || this.element.classList.contains('hidden')) {
return;
}
this.debug('hidden the entire app window.');
this.element.classList.add('hidden');
this.publish('hidden');
};
AppWindow.prototype.queueShow = function aw_queueShow() {
this.element.classList.add('will-become-active');
// bug 1033921: notify current app changed
this.publish('will-become-active');
};
AppWindow.prototype.cancelQueuedShow = function aw_cancelQueuedShow() {
this.element.classList.remove('will-become-active');
};
AppWindow.prototype.queueHide = function aw_queueHide() {
this.element.classList.add('will-become-inactive');
this.publish('will-become-inactive');
};
/**
* Wait for a full repaint of the mozbrowser iframe.
*/
AppWindow.prototype.tryWaitForFullRepaint = function onTWFRepaint(callback) {
if (!callback) {
return;
}
if (this.isHomescreen) {
setTimeout(callback);
return;
}
this.debug('trying wait for full repaint by screenshot enforcing..');
this.getScreenshot(function() {
setTimeout(callback);
}, 1, 1, 400);
};
/**
* Request a screenshot ObjectURL temporarily.
* The image would be discarded after 200ms or the revoke callback
* is invoked.
*/
AppWindow.prototype.requestScreenshotURL =
function aw__requestScreenshotURL() {
if (this.frontWindow) {
return this.frontWindow.requestScreenshotURL();
}
if (!this._screenshotBlob) {
return null;
}
var screenshotURL = URL.createObjectURL(this._screenshotBlob);
setTimeout(function onTimeout() {
if (screenshotURL) {
URL.revokeObjectURL(screenshotURL);
screenshotURL = null;
}
}, 200);
return screenshotURL;
};
/**
* Show screenshot overlay and hide the iframe.
*/
AppWindow.prototype._showScreenshotOverlay =
function aw__showScreenshotOverlay() {
if (!this.screenshotOverlay ||
this.screenshotOverlay.classList.contains('visible')) {
return;
}
if (this.identificationOverlay) {
this.identificationOverlay.classList.add('visible');
}
this.screenshotOverlay.classList.add('visible');
var screenshotURL = this.requestScreenshotURL();
this.screenshotOverlay.style.backgroundImage =
'url(' + screenshotURL + ')';
};
/**
* Check if current visibility state is screenshot or not,
* to hide the screenshot overlay.
*/
AppWindow.prototype._hideScreenshotOverlay =
function aw__hideScreenshotOverlay() {
if (!this.screenshotOverlay ||
!this.screenshotOverlay.classList.contains('visible')) {
return;
}
this.screenshotOverlay.classList.remove('visible');
this.screenshotOverlay.style.backgroundImage = '';
if (this.identificationOverlay) {
var overlay = this.identificationOverlay;
// A white flash can occur when removing the screenshot
// so we trigger this transition after a tick to hide it.
setTimeout(function nextTick() {
overlay.classList.remove('visible');
});
}
};
// Get cached screenshot Blob if there is one.
// Note: the caller should revoke the created ObjectURL once it's finishing.
AppWindow.prototype.getCachedScreenshotBlob =
function aw_getCachedScreenshotBlob() {
return this._screenshotBlob;
};
// Save and update screenshot Blob.
AppWindow.prototype.renewCachedScreenshotBlob =
function aw_renewScreenshot(screenshotBlob) {
this._screenshotBlob = screenshotBlob;
};
/**
* Event prefix presents the object type
* when publishing an event from the element.
* Always 'app' for now.
*
* @type {String}
*/
AppWindow.prototype.eventPrefix = 'app';
/**
* Publish an event.
*
* @param {String} event Event name, without object type prefix.
* @param {Object} detail Parameters in JSON format.
*/
AppWindow.prototype.publish = function(event, detail) {
// Dispatch internal event before external events.
this.broadcast(event, detail);
var evt = new CustomEvent(this.eventPrefix + event,
{
bubbles: true,
detail: detail || this
});
this.debug('publishing external event: ' + event +
(detail ? JSON.stringify(detail) : ''));
// Publish external event.
if (this.rearWindow && this.element) {
this.element.dispatchEvent(evt);
} else {
window.dispatchEvent(evt);
}
};
AppWindow.prototype.broadcast = function aw_broadcast(event, detail) {
// Broadcast internal event.
if (this.element) {
var internalEvent = new CustomEvent('_' + event,
{
bubbles: false,
detail: detail || this
});
this.debug('publishing internal event: ' + event);
this.element.dispatchEvent(internalEvent);
}
};
var OrientationRotationArray = [
'portrait-primary', 'portrait-secondary', 'portrait',
'landscape-primary', 'landscape-secondary', 'landscape', 'default'];
var OrientationRotationTable = {
'portrait-primary': [0, 180, 0, 90,
270, 90, OrientationManager.isDefaultPortrait() ? 0 : 90],
'landscape-primary': [270, 90, 270, 0,
180, 0, OrientationManager.isDefaultPortrait() ? 270 : 0],
'portrait-secondary': [180, 0, 180, 270,
90, 270, OrientationManager.isDefaultPortrait() ? 180 : 270],
'landscape-secondary': [90, 270, 90, 180,
0, 180, OrientationManager.isDefaultPortrait() ? 180 : 90]
};
AppWindow.prototype.determineRotationDegree =
function aw__determineRotationDegree() {
if (!this.manifest) {
return 0;
}
var appOrientation = this.manifest.orientation;
var orientation = this.determineOrientation(appOrientation);
var table =
OrientationRotationTable[
OrientationManager.defaultOrientation];
var degree = table[OrientationRotationArray.indexOf(orientation)];
this.rotatingDegree = degree;
if (degree == 90 || degree == 270) {
this.element.classList.add('perpendicular');
}
return degree;
};
AppWindow.prototype.determineClosingRotationDegree =
function aw__determineClosingRotationDegree() {
if (!this.manifest) {
return 0;
}
// XXX: Assume homescreen's orientation is just device default.
var homeOrientation = OrientationManager.defaultOrientation;
var currentOrientation = OrientationManager
.fetchCurrentOrientation();
this.debug(currentOrientation);
var table = OrientationRotationTable[homeOrientation];
var degree = table[OrientationRotationArray.indexOf(currentOrientation)];
return Math.abs(360 - degree) % 360;
};
/**
* All apps in TV are fullscreen. We keep this function for future extension.
* @return {Boolean} We're a fullscreen app or not.
*/
AppWindow.prototype.isFullScreen = function aw_isFullScreen() {
return true;
};
/**
* Detect whether this app is resized 100% width and height by its manifest.
* The difference between fullscreen and fullscreen_layout is the software
* home button. And we don't have it in TV. So, we just make them are all
* fullscreen and fullscreen_layout. There is another difference between them
* that fullscreen_layout is used for the mozRequestFullScreen.
* @return {Boolean} We're a fullscreen_layout app or not.
*/
AppWindow.prototype.isFullScreenLayout = function aw_isFullScreenLayout() {
return true;
};
AppWindow.prototype._defaultOrientation = null;
AppWindow.prototype.determineOrientation =
function aw_determineOrientation(orientation) {
if (this._defaultOrientation) {
return this._defaultOrientation;
} else if (!orientation) {
this._defaultOrientation = 'default';
return this._defaultOrientation;
}
if (!Array.isArray(orientation)) {
orientation = [orientation];
}
this._defaultOrientation = orientation[0];
return this._defaultOrientation;
};
AppWindow.prototype._resize = function aw__resize() {
var height, width;
this.debug('force RESIZE...');
if (layoutManager.keyboardEnabled) {
/**
* The event is dispatched on the app window only when keyboard is up.
*
* @access private
* @event AppWindow~_withkeyboard
*/
this.broadcast('withkeyboard');
} else {
/**
* The event is dispatched on the app window only when keyboard is hidden.
*
* @access private
* @event AppWindow~_withoutkeyboard
*/
this.broadcast('withoutkeyboard');
}
height = layoutManager.height;
// If we have sidebar in the future, change layoutManager then.
width = layoutManager.width;
this.width = width;
this.height = height;
this.element.style.width = this.width + 'px';
this.element.style.height = this.height + 'px';
this.iframe.style.width = '';
this.iframe.style.height = '';
this.resized = true;
if (this.screenshotOverlay) {
this.screenshotOverlay.style.visibility = '';
}
/**
* Fired when the app is resized.
*
* @event AppWindow#appresize
*/
this.publish('resize');
this.debug('W:', this.width, 'H:', this.height);
};
/**
* Set the size of the app's iframe to match the size of the screen.
* We have to call this on resize events (which happen when the
* phone orientation is changed). And also when an app is launched
* and each time an app is brought to the front, since the
* orientation could have changed since it was last displayed
*
* An appWindow instance only resizes if it's active.
* Since we already pre-config the size in window.css,
* if an app is launched by system message we don't need to resize
* it.
*
* 
*/
AppWindow.prototype.resize = function aw_resize() {
if (this.isDead()) {
return;
}
this.debug('request RESIZE...active? ', this.isActive());
var bottom = this.getBottomMostWindow();
if (!bottom.isActive() || this.isTransitioning()) {
return;
}
var top = this.getTopMostWindow();
if (top.instanceID != this.instanceID) {
bottom._resize();
top.resize();
} else {
// resize myself if no child.
this.debug(' will resize... ');
this._resize();
}
};
AppWindow.prototype.getTopMostWindow = function() {
var win = this;
while (win.frontWindow) {
win = win.frontWindow;
}
return win;
};
AppWindow.prototype.getBottomMostWindow = function() {
var win = this;
while (win.rearWindow) {
win = win.rearWindow;
}
return win;
};
/**
* Lock the orientation for this app anyway.
*/
AppWindow.prototype.lockOrientation = function() {
var manifest = this.manifest || this.config.manifest;
var orientation = manifest ? (manifest.orientation ||
OrientationManager.globalOrientation) :
OrientationManager.globalOrientation;
if (orientation) {
var rv = screen.mozLockOrientation(orientation);
if (rv === false) {
console.warn('screen.mozLockOrientation() returned false for',
this.origin, 'orientation', orientation);
} else {
this.debug(' locking screen orientation to ' + orientation);
}
} else { // If no orientation was requested, then let it rotate
screen.mozUnlockOrientation();
this.debug(' Unlocking screen orientation..');
}
};
/**
* Lock or unlock orientation for this app.
* Note: if we have front window, the request would be delivered
* to the front active window.
*/
AppWindow.prototype.setOrientation =
function aw_setOrientation() {
if (!this.getBottomMostWindow().isActive()) {
return;
}
if (this.frontWindow && this.frontWindow.isActive()) {
this.frontWindow.setOrientation();
} else {
this.lockOrientation();
}
};
/**
* fade out to window to black.
*/
AppWindow.prototype.fadeOut = function aw__fadeout() {
if (!this.isActive() && this.element) {
this.element.classList.add('fadeout');
this.debug(' fade out >>>> ');
}
};
/**
* fade in the window from black.
*/
AppWindow.prototype.fadeIn = function aw__fadein() {
if (this.isActive()) {
this.element.classList.remove('fadeout');
this.debug(' fade in <<<<< ');
}
};
AppWindow.prototype.setCalleeWindow =
function aw_setCalleeWindow(callee) {
this.calleeWindow = callee;
callee.callerWindow = this;
};
AppWindow.prototype.unsetCalleeWindow =
function aw_unsetCalleeWindow() {
if (this.calleeWindow.callerWindow) {
this.calleeWindow.callerWindow = null;
}
this.calleeWindow = null;
};
AppWindow.prototype.unsetAttentionWindow =
function aw_unsetAttentionWindow() {
this.attentionWindow = null;
};
/**
* Modify an attribute on this.element
* @param {String} type State type.
* @param {String} state State name.
*/
AppWindow.prototype._changeState = function aw__changeState(type, state) {
if (this.element) {
this.element.setAttribute(type + '-state', state.toString());
}
};
/**
* Mixin the appWindow prototype with {mixin} object.
* @param {Object} mixin The object to be mixed.
*/
AppWindow.addMixin = function AW_addMixin(mixin) {
for (var prop in mixin) {
if (mixin.hasOwnProperty(prop)) {
// Put event handler function into an array,
// if the name of the propery is '_on'.
if (!this.prototype.hasOwnProperty(prop)) {
this.prototype[prop] = mixin[prop];
}
}
}
};
AppWindow.prototype.preloadSplash = function aw_preloadSplash() {
if (this._splash || this.config.icon) {
if (this.config.icon) {
this._splash = this.config.icon;
} else {
// origin might contain a pathname too, so need to parse it to find the
// "real origin"
var url = this.config.origin.split('/');
var origin = url[0] + '//' + url[2];
this._splash = origin + this._splash;
}
// Start to load the image in background to avoid flickering if possible.
var img = new Image();
img.src = this._splash;
}
};
/**
* The preferred CSS size of the icon used for cold launch splash for phones.
*/
AppWindow.prototype.SPLASH_ICON_SIZE_TINY = 60;
/**
* The preferred CSS size of the icon used for cold launch splash for
* other devices.
*/
AppWindow.prototype.SPLASH_ICON_SIZE_NOT_TINY = 90;
AppWindow.prototype.getIconForSplash =
function aw_getIconForSplash(manifest) {
var icons = this.manifest ?
('icons' in this.manifest ? this.manifest.icons : null) : null;
if (!icons) {
return null;
}
var targetedPixelSize = 2 * (ScreenLayout.getCurrentLayout('tiny') ?
this.SPLASH_ICON_SIZE_TINY : this.SPLASH_ICON_SIZE_NOT_TINY) *
Math.ceil(window.devicePixelRatio || 1);
var preferredSize = Number.MAX_VALUE;
var max = 0;
for (var size in icons) {
size = parseInt(size, 10);
if (size > max) {
max = size;
}
if (size >= targetedPixelSize && size < preferredSize) {
preferredSize = size;
}
}
// If there is an icon matching the preferred size, we return the result,
// if there isn't, we will return the maximum available size.
if (preferredSize === Number.MAX_VALUE) {
preferredSize = max;
}
this._splash = icons[preferredSize];
this.preloadSplash();
return icons[preferredSize];
};
/**
* Set splash as our inital background.
* If the content is loaded then just skip.
*/
AppWindow.prototype.setFrameBackground =
function aw_setFrameBackground() {
if (!this.isHomescreen &&
!this.loaded && !this.splashed && this.element) {
if (this._splash) {
this.splashed = true;
this.element.style.backgroundImage = 'url("' + this._splash + '")';
var iconCSSSize = 2 * (ScreenLayout.getCurrentLayout('tiny') ?
this.SPLASH_ICON_SIZE_TINY : this.SPLASH_ICON_SIZE_NOT_TINY);
this.element.style.backgroundSize =
iconCSSSize + 'px ' + iconCSSSize + 'px';
if (this.identificationIcon) {
this.identificationIcon.style.backgroundImage =
'url("' + this._splash + '")';
}
}
if (this.identificationTitle) {
this.identificationTitle.textContent = this.name;
}
}
};
/**
* The app window is requesting to be opened.
*
* @fires AppWindow#apprequestopen
*/
AppWindow.prototype.requestOpen = function aw_requestOpen() {
/**
* Fired once we'd like to be opened.
* The one who makes decision to call open() would be AppWindowManager.
*
* @event AppWindow#apprequestopen
*/
this.publish('requestopen');
};
/**
* The app window is requesting to be closed.
*
* @fires AppWindow#apprequestclose
*/
AppWindow.prototype.requestClose = function aw_requestClose() {
/**
* Fired once we'd like to be closed.
* The one who makes decision to call close() would be AppWindowManager.
*
* @event AppWindow#apprequestclose
*/
this.publish('requestclose');
};
/**
* Change URL only when we're at background.
* @param {String} url URL.
*/
AppWindow.prototype.modifyURLatBackground = function aw_changeURL(url) {
// XXX: If the app is in foreground, it's too risky to change it's
// URL. We still change it because the home app is an overlay on top of any
// app.
var iframe = this.browser.element;
// If the app is opened and it is loaded to the correct page,
// then there is nothing to do.
if (iframe.src !== url) {
// Rewrite the URL of the app frame to the requested URL.
// XXX: We could ended opening URLs not for the app frame
// in the app frame. But we don't care.
iframe.src = url;
}
};
/**
* Do resize and/or ensure repaint before opening the app.
* @param {Function} callback Callback when app is ready to be opened.
*/
AppWindow.prototype.ready = function aw_ready(callback) {
if (!this.element) {
return;
}
this.debug('requesting to open');
if (!this.loaded ||
(this.screenshotOverlay &&
this.screenshotOverlay.classList.contains('visible'))) {
this.debug('loaded yet');
setTimeout(callback);
return;
} else {
var invoked = false;
this.waitForNextPaint(function() {
if (invoked) {
return;
}
invoked = true;
setTimeout(callback);
});
if (this.isHomescreen) {
this.setVisible(true);
return;
}
this.tryWaitForFullRepaint(function() {
if (invoked) {
return;
}
invoked = true;
setTimeout(callback);
});
}
};
/**
* Open the window; the detail is done in appTransitionController.
* @param {String} animation The animation class name.
*/
AppWindow.prototype.open = function aw_open(animation) {
// Request "open" to our internal transition controller.
if (this.transitionController) {
this.transitionController.requireOpen(animation);
}
};
/**
* Close the window; the detail is done in appTransitionController.
* @param {String} animation The animation class name.
*/
AppWindow.prototype.close = function aw_close(animation) {
// Request "close" to our internal transition controller.
if (this.transitionController) {
this.transitionController.requireClose(animation);
}
};
AppWindow.prototype._handle__swipein = function aw_swipein() {
// Revive the browser element if it's got killed in background.
this.reviveBrowser();
// Request "open" to our internal transition controller.
if (this.transitionController) {
this.transitionController.switchTransitionState('opened');
this.publish('opened');
}
};
AppWindow.prototype._handle__swipeout = function aw_swipeout() {
// Request "close" to our internal transition controller.
if (this.transitionController) {
this.transitionController.switchTransitionState('closed');
this.publish('closed');
}
};
AppWindow.prototype._handle__sheetsgesturebegin = function aw_sgbegin() {
// If we're the active we shouldn't do anything at this point. We'll get
// our frame hidden on swipeout.
if (this.isActive()) {
this.debug('no screenshot for active app during sheetsgesturebegin');
return;
}
// For inactive apps we'll already have a screenshot blob ready for use.
this.debug('showing screenshot during sheetsgesturebegin');
this._showScreenshotOverlay();
};
AppWindow.prototype._handle__sheetsgestureend = function aw_sgend() {
if (this.isActive()) {
this.debug('nothing to do during sheetsgestureend');
return;
}
this.debug('hiding screenshot on sheetsgestureend');
this._hideScreenshotOverlay();
};
AppWindow.prototype._handle__cardviewbeforeshow = function aw_cvbeforeshow() {
this.debug('showing screenshot for cardsview.');
this._showScreenshotOverlay();
};
AppWindow.prototype._handle__cardviewclosed = function aw_cvclosed() {
this.debug('hiding screenshot after cardsview closed.');
this._hideScreenshotOverlay();
};
AppWindow.prototype._handle__closed = function aw_closed() {
//
// Never take screenshots of the homescreen.
//
// We don't need the screenshot of homescreen because:
// 1. Homescreen background is transparent,
// currently gecko only sends JPG to us.
// See bug 878003.
// 2. Homescreen screenshot isn't required by card view.
// Since getScreenshot takes additional memory usage,
// let's early return here.
// 3. We want to remove this long term, see bug 1072781.
//
if (this.getBottomMostWindow().isHomescreen) {
return;
}
// Update screenshot blob here to avoid slowing down closing transitions.
this.getScreenshot();
};
AppWindow.prototype._handle__kill_suspended = function aw() {
if (this.suspended) {
this.kill();
}
};
/**
* Link child window to this app window instance.
* If there's already one, kill it at first.
* @param {ChildWindow} childWindow The child window instance.
*/
AppWindow.prototype.setNextWindow = function aw_setNextWindow(nextWindow) {
if (this.nextWindow) {
console.warn('There is already alive child window, killing...',
this.nextWindow.instanceID);
this.nextWindow.kill();
}
this.nextWindow = nextWindow;
};
AppWindow.prototype.unsetNextWindow = function aw_unsetNextWindow() {
if (this.nextWindow) {
this.nextWindow = null;
}
};
/**
* Build bottom/top window relationship.
* If there's already one, kill it at first.
* @param {AppWindow} fronWindow The front window instance.
*/
AppWindow.prototype.setFrontWindow = function aw_setFrontWindow(frontWindow) {
if (this.frontWindow) {
console.warn('There is already alive child window, killing...',
this.frontWindow.instanceID);
this.frontWindow.kill();
}
this.frontWindow = frontWindow;
};
AppWindow.prototype.unsetFrontWindow = function aw_unsetFrontWindow() {
if (this.frontWindow) {
this.frontWindow = null;
}
};
/**
* Get the previous window reference of current active one.
* @return {AppWindow} The previous one in the app sheet chain.
*/
AppWindow.prototype.getPrev = function() {
var current = this.getActiveWindow();
if (current) {
return current.previousWindow;
} else {
return null;
}
};
/**
* Get the next window reference of current active one.
* @return {AppWindow} The next one in the app sheet chain.
*/
AppWindow.prototype.getNext = function() {
var current = this.getActiveWindow();
if (current) {
return current.nextWindow;
} else {
return null;
}
};
/**
* Get the active window reference in the app sheet chain.
* If there's no active window, return null.
* @return {AppWindow} The active window in the app sheet chain.
*/
AppWindow.prototype.getActiveWindow = function() {
var app = this;
while (app) {
if (app.isActive()) {
return app;
}
app = app.nextWindow;
}
return null;
};
/**
* Get the root window reference.
* If there's no parent, return ourself.
* @return {AppWindow} The first one in the app sheet chain.
*/
AppWindow.prototype.getRootWindow = function() {
var app = this;
while (app.previousWindow) {
app = app.previousWindow;
}
return app;
};
/**
* Get the leaf window reference.
* If there's no child, return ourself.
* @return {AppWindow} The last one in the app sheet chain.
*/
AppWindow.prototype.getLeafWindow = function() {
var app = this;
while (app.nextWindow) {
app = app.nextWindow;
}
return app;
};
AppWindow.prototype.getFrameForScreenshot = function() {
var top = this.getTopMostWindow();
return top.browser ? top.browser.element : null;
};
/**
* Indicate we are in viewport or not.
* @return {Boolean} We are in viewport or outside viewport.
*/
AppWindow.prototype.isVisible = function() {
var bottomMostWindow = this.getBottomMostWindow();
return bottomMostWindow.isActive() && this.isActive();
};
/**
* Apply a transform to the element
* @param {Object} nameValues object with transform property names as keys
* and values to apply to the element
* @memberOf AppWindow.prototype
*/
AppWindow.prototype.transform = function(nameValues) {
var strFunctions = Object.keys(nameValues).map(function(key) {
return key + '(' + nameValues[key] + ')';
}, this).join(' ');
this.applyStyle({ MozTransform: strFunctions });
};
/**
* Batch apply style properties
* @param {Object} nameValues object with style property names as keys
* and values to apply to the element
* @memberOf AppWindow.prototype
*/
AppWindow.prototype.applyStyle = function(nameValues) {
var dirty = this._dirtyStyleProperties || (this._dirtyStyleProperties = {});
var style = this.element.style;
for (var property in nameValues) {
if (undefined === nameValues[property]) {
delete style[[property]];
} else {
style[property] = nameValues[property];
}
dirty[property] = true;
}
};
/**
* Remove inline style properties
* @param {Object} nameValues object with style property names as keys
* @memberOf AppWindow.prototype
*/
AppWindow.prototype.unapplyStyle = function(nameValues) {
var style = this.element.style;
for (var pname in nameValues) {
style[pname] = '';
delete style[pname];
}
};
/**
* Show the default contextmenu for an AppWindow
* @memberOf AppWindow.prototype
*/
AppWindow.prototype.showDefaultContextMenu = function() {
if (this.contextmenu) {
this.contextmenu.showDefaultMenu();
}
};
/**
* Hide the contextmenu for an AppWindow
* @memberOf AppWindow.prototype
*/
AppWindow.prototype.hideContextMenu = function() {
if (this.contextmenu) {
this.contextmenu.hide();
}
};
AppWindow.prototype._handle__focus = function() {
focusManager.focus();
};
/**
* Request to be foreground to the visibilityManager.
* If it's fine to let us to be active, this.setVisible(true) will be called.
*/
AppWindow.prototype.requestForeground =
function aw_requestForeground() {
this.publish('requestforeground');
};
/**
* The window is killable by user manual action or not.
* @return {Boolean} The app is killable or not.
*/
AppWindow.prototype.killable = function() {
// This property is updated whenever an attentionWindow
// is created or destroyed.
if (this.attentionWindow || this.isHomescreen) {
return false;
} else {
return true;
}
};
AppWindow.prototype.hasPermission =
function aw_hasPermission(name) {
if (typeof(this._hasPermission) !== 'undefined' &&
this._hasPermission[name]) {
return this._hasPermission[name];
}
var mozPerms = navigator.mozPermissionSettings;
if (!mozPerms || !this.manifestURL) {
return false;
}
var value =
mozPerms.get(name, this.manifestURL, this.origin, false);
if (!this._hasPermission) {
this._hasPermission = {};
}
this._hasPermission[name] = (value === 'allow');
return this._hasPermission[name];
};
AppWindow.prototype.isHidden = function() {
return !this.element || this.element.classList.contains('hidden');
};
/**
* _hidewindow event handler.
*
* The event occurs when there's a higher priority window
* which is not an AppWindow show up.
* AppWindowManager will redirect the event to the current
* active app.
*
* If the event is because of attention window coming,
* evt.detail will be the instance of the attention window.
* If we are the opener of the attention window,
* we should not be sent to background due to
* @param {Event} evt The hidewindow event
*/
AppWindow.prototype._handle__hidewindow = function (evt) {
var attention = evt.detail;
if (attention.parentWindow &&
attention.parentWindow.instanceID === this.instanceID) {
return;
}
this.setVisible(false);
};
AppWindow.prototype.getElement = function() {
return this.element;
};
AppWindow.prototype.isFocusable = function() {
return this.loaded && this.isVisible();
};
exports.AppWindow = AppWindow;
}(window));