define(function(require, exports, module) { 'use strict'; /** * Dependencies */ var formatRecorderProfiles = require('lib/format-recorder-profiles'); var formatPictureSizes = require('lib/format-picture-sizes'); var debug = require('debug')('controller:settings'); var bindAll = require('lib/bind-all'); /** * Exports */ module.exports = function(app) { return new SettingsController(app); }; module.exports.SettingsController = SettingsController; /** * Initialize a new `SettingsController` * * @constructor * @param {App} app */ function SettingsController(app) { bindAll(this); this.app = app; this.require = app.require; this.settings = app.settings; this.activity = app.activity; this.notification = app.views.notification; // Provide test hooks this.nav = app.nav || navigator; this.SettingsView = app.SettingsView; this.formatPictureSizes = app.formatPictureSizes || formatPictureSizes; this.formatRecorderProfiles = app.formatRecorderProfiles || formatRecorderProfiles; this.configure(); this.bindEvents(); debug('initialized'); } /** * Registers settings 'aliases' that provide * a single interface for settings such as * `flashModes` where we have `flashModesPicture` * and `flashModesVideo`. * * This means that we can just use settings.flashModes, * and be confident that it will interface with the * correct setting depending on the value of `mode`. * * You can always use the underlying settings * directly if you need that kind of control. */ SettingsController.prototype.configure = function() { this.setupRecorderProfilesAlias(); this.setupPictureSizesAlias(); this.setupFlashModesAlias(); }; /** * Creates a SettingAlias that dynamically * interfaces with the correct recorder * profile Setting based on which camera * ('front'/'back') is selected. * * @private */ SettingsController.prototype.setupRecorderProfilesAlias = function() { var settings = this.settings; this.settings.alias({ key: 'recorderProfiles', settings: { back: this.settings.recorderProfilesBack, front: this.settings.recorderProfilesFront }, get: function() { var camera = settings.cameras.selected('key'); return this.settings[camera]; } }); }; /** * Creates a SettingAlias that dynamically * interfaces with the correct picture * size Setting based on which camera * ('front'/'back') is selected. * * @private */ SettingsController.prototype.setupPictureSizesAlias = function() { var settings = this.settings; this.settings.alias({ key: 'pictureSizes', settings: { back: this.settings.pictureSizesBack, front: this.settings.pictureSizesFront }, get: function() { var camera = settings.cameras.selected('key'); return this.settings[camera]; } }); }; /** * Creates a SettingAlias that dynamically * interfaces with the correct flash-modes * Setting based on which mode ('picture'/ * 'video') is selected. * * @private */ SettingsController.prototype.setupFlashModesAlias = function() { var settings = this.settings; this.settings.alias({ key: 'flashModes', settings: { picture: this.settings.flashModesPicture, video: this.settings.flashModesVideo }, get: function() { var mode = settings.mode.selected('key'); return this.settings[mode]; } }); }; /** * Bind to app events. * * @private */ SettingsController.prototype.bindEvents = function() { this.app.on('localized', this.formatPictureSizeTitles); this.app.on('settings:toggle', this.toggleSettings); this.app.on('camera:newcamera', this.onNewCamera); this.app.on('activity:pick', this.onPickActivity); }; /** * Toggle the settings menu open/closed. * * @private */ SettingsController.prototype.toggleSettings = function() { if (this.view) { this.closeSettings(); } else { this.openSettings(); } }; /** * Render and display the settings menu. * * We use settings.menu() to retrieve * and ordered list of settings that * have a `menu` property. * * @private */ SettingsController.prototype.openSettings = function() { debug('open settings'); var self = this; this.app.emit('busy', 'lazyLoading'); this.require(['views/settings'], function(SettingsView) { self.app.emit('ready'); if (!self.SettingsView) { self.SettingsView = SettingsView; } if (self.view) { return; } var items = self.menuItems(); self.view = new self.SettingsView({ items: items }) .render() .appendTo(self.app.el) .on('click:close', self.closeSettings) .on('click:option', self.onOptionTap); // Make sure the view is // hidden before fading in self.view.hide(); self.view.fadeIn(); self.app.emit('settings:opened'); debug('settings opened'); }); }; /** * Destroy the settings menu. * * @private */ SettingsController.prototype.closeSettings = function(done) { debug('close settings'); if (!this.view) { return; } var self = this; this.view.fadeOut(function() { self.view.destroy(); self.view = null; self.app.emit('settings:closed'); debug('settings closed'); if (typeof done === 'function') { done(); } }); }; /** * Selects the option that was * clicked on the setting. * * @param {String} key * @param {Setting} setting * @private */ SettingsController.prototype.onOptionTap = function(key, setting) { var flashMode = this.settings.flashModesPicture.selected('key'); var ishdrOn = setting.key === 'hdr' && key === 'on'; var flashDeactivated = flashMode !== 'off' && ishdrOn; var self = this; self.closeSettings(function() { setting.select(key); self.notify(setting, flashDeactivated); }); }; /** * Adjusts settings to meet requirements * on new pick activity. * * @param {Object} data * @private */ SettingsController.prototype.onPickActivity = function(data) { debug('pick activity', data); var setting; var options; var updated = false; var maxFileSize = data.maxFileSizeBytes; var maxPixelSize = data.maxPixelSize; // Settings changes made in 'pick' // sessions shouldn't persist. this.settings.dontSave(); if (maxPixelSize) { setting = this.settings.pictureSizes; var lastMaxPixelSize = setting.get('maxPixelSize'); this.settings.pictureSizesFront.set('maxPixelSize', maxPixelSize); this.settings.pictureSizesBack.set('maxPixelSize', maxPixelSize); debug('set maxPixelSize: %s', maxPixelSize); if (lastMaxPixelSize !== maxPixelSize) { options = setting.get('options'); var restricted = []; if (options && options.length > 0) { options.forEach(function(option) { if (option.pixelSize <= maxPixelSize) { restricted.push(option); } }); setting.resetOptions(restricted); updated = true; } } } if (maxFileSize) { setting = this.settings.recorderProfiles; var lastMaxFileSize = setting.get('maxFileSizeBytes'); this.settings.recorderProfilesFront.set('maxFileSizeBytes', maxFileSize); this.settings.recorderProfilesBack.set('maxFileSizeBytes', maxFileSize); debug('set maxFileSize: %s', maxFileSize); if (lastMaxFileSize !== maxFileSize) { options = setting.get('options'); if (options && options.length > 1) { setting.resetOptions([options[options.length - 1]]); updated = true; } } } // If the size restrictions come in after the camera was brought // up, then we must retrigger a configuration event if (updated) { this.app.emit('settings:configured'); } }; /** * Display a notifcation showing the * current state of the given setting. * * If `notification` is `false in config * for a setting then we don't show one. * * @param {Setting} setting * @private */ SettingsController.prototype.notify = function(setting, flashDeactivated) { var dontNotify = setting.get('notifications') === false; if (dontNotify) { return; } var localizable = setting.get('optionsLocalizable') !== false; var title = ''; var html; // Localize option title only if not specified in the config var optionTitle = localizable ? '' : '' + setting.selected('title') + ''; // Check if the `flashMode` setting is going to be deactivated as part // of the change in the `hdr` setting and display a specialized // notification if that is the case if (flashDeactivated) { html = title + ' ' + optionTitle + '
' + ''; } else { html = title + '
' + optionTitle; } this.notification.display({ text: { html: html }}); }; /** * When the hardware capabilities change * we update the available options for * each setting to match what is available. * * The rest of the app should listen for * the 'settings:configured' event as an * indication to update UI etc. * * @param {Object} capabilities */ SettingsController.prototype.onNewCamera = function(camera) { debug('new capabilities'); var capabilities = camera.capabilities; this.settings.hdr.filterOptions(capabilities.hdr); this.settings.flashModesPicture.filterOptions(capabilities.flashModes); this.settings.flashModesVideo.filterOptions(capabilities.flashModes); this.configurePictureSizes(camera); this.configureRecorderProfiles(camera); // Let the rest of the app know we're good to go. this.app.emit('settings:configured'); debug('settings configured to new capabilities'); }; /** * Formats the raw pictureSizes into * a format that Setting class can * work with, then resets the pictureSize * options. * * @param {Array} sizes */ SettingsController.prototype.configurePictureSizes = function(camera) { debug('configuring picture sizes'); var maxPixelSize = window.CONFIG_MAX_IMAGE_PIXEL_SIZE; var sizes = camera.capabilities.pictureSizes; var setting = this.settings.pictureSizes; var currentSize = camera.pictureSize; var currentSizeKey = currentSize.width + 'x' + currentSize.height; var options = { exclude: setting.get('exclude'), include: setting.get('include'), maxPixelSize: maxPixelSize }; var formatted = this.formatPictureSizes(sizes, options); // A sensor may expose only sizes outside Gaia's original 4:3 preset. if (!formatted.length && sizes.length) { formatted = this.formatPictureSizes(sizes, {}); } setting.resetOptions(formatted); // If the setting has no stored default // set the setting to match the current // mozCamera setting. if (!setting.current().fetched) { setting.select(currentSizeKey, { silent: true }); } this.formatPictureSizeTitles(); debug('configured pictureSizes', setting.selected('key')); }; /** * Formats the raw recorderProfiles * into a format that Setting class can * work with, then resets the recorderProfile * options. * * @param {Array} sizes */ SettingsController.prototype.configureRecorderProfiles = function(camera) { var sizes = camera.capabilities.recorderProfiles; var currentProfile = camera.recorderProfile; var setting = this.settings.recorderProfiles; var maxFileSize = setting.get('maxFileSizeBytes'); var exclude = setting.get('exclude'); var options = { exclude: exclude }; var items = this.formatRecorderProfiles(sizes, options); // If a file size limit has been imposed, // pick the lowest-res (last) profile only. if (maxFileSize) { items = [items[items.length - 1]]; } setting.resetOptions(items); // Set the recorder profile to match // the currently selected mozCamera profile if (!maxFileSize && !setting.current().fetched) { setting.select(currentProfile, { silent: true }); } }; /** * Creates a localized `title` property * on each pictureSize option. This is * used within the settings-menu. * * This is run each time `configurePictureSizes` * is run and each time the app recieves a * 'localized' event. * * If the app isn't 'localized' yet, we don't do * anything and wait for the 'localized' * event binding to run the function. * * @private */ SettingsController.prototype.formatPictureSizeTitles = function() { return document.l10n.formatValue('mp').then((value) => { var options = this.settings.pictureSizes.get('options'); options.forEach(function(size) { var data = size.data; var mp = data.mp ? data.mp + value + ' ' : ''; size.title = mp + size.key; }); debug('picture size titles formatted'); }); }; /** * Returns a list of settings * based on the `settingsMenu` * configuration. * * @return {Array} */ SettingsController.prototype.menuItems = function() { var items = this.settings.settingsMenu.get('items'); return items.filter(this.validMenuItem, this) .map(function(item) { return this.settings[item.key]; }, this); }; /** * Tests if the passed `settingsMenu` * item is allowed in the settings menu. * * @param {Object} item * @return {Boolean} */ SettingsController.prototype.validMenuItem = function(item) { var setting = this.settings[item.key]; return !!setting && setting.supported(); }; });