Source: js/hierarchy_manager.js

/* global BaseModule */
'use strict';

(function() {
  var HierarchyManager = function() {};
  HierarchyManager.SERVICES = [
    'focus',
    'registerHierarchy',
    'unregisterHierarchy'
  ];
  HierarchyManager.STATES = [
    'getTopMostWindow',
    'getTopMostUI'
  ];
  HierarchyManager.EVENTS = [
    'home',
    'holdhome',
    'system-resize',
    'launchactivity',
    'mozChromeEvent',
    'windowopened',
    'windowclosed'
  ];
  BaseModule.create(HierarchyManager, {
    name: 'HierarchyManager',
    EVENT_PREFIX: 'hierarchy',
    _ui_list: null,
    _topMost: null,
    DEBUG: false,

    _start: function() {
      this._ui_list = [];
    },

    /**
     * Provide the top most window information.
     * Usually used by integration tests.
     * @return {AppWindow|undefined} The top most window instance.
     */
    getTopMostWindow: function() {
      var topMostWindowManager;
      this._ui_list.some(function(module) {
        if (module.getActiveWindow && module.isActive()) {
          topMostWindowManager = module;
          return true;
        }
      }, this);
      return topMostWindowManager &&
             topMostWindowManager.getActiveWindow() &&
             topMostWindowManager.getActiveWindow().getTopMostWindow();
    },

    getTopMostUI: function() {
      this.debug('getting top most...', this._topMost);
      return this._topMost;
    },

    /**
     * Predefined priorities per module by module name.
     * @type {Array}
     */
    PRIORITIES: [
      'OverlayWindowManager',
      'CoverScreen',
      'AttentionWindowManager',
      'SecureWindowManager',
      'LockScreenWindowManager',
      'UtilityTray',
      'Rocketbar',
      'SystemDialogManager',
      'AppWindowManager',
      'TaskManager'
    ],

    _stop: function() {
      this._ui_list.forEach(function(module) {
        window.removeEventListener(module + 'active', this);
        window.removeEventListener(module + 'inactive', this);
      }, this);
      this._ui_list = [];
    },

    updateHierarchy: function() {
      if (this._ui_list.length === 0) {
        this.debug('no any module watching.');
        return;
      }
      var lastTopMost = this._topMost;
      this._topMost = null;
      var found = this._ui_list.some(function(module) {
        if (module.isActive()) {
          this.debug(module.name + ' is becoming active now.');
          this._topMost = module;
          return true;
        }
      }, this);

      if (this._topMost !== lastTopMost) {
        if (lastTopMost) {
          this.debug('last top most is ' + lastTopMost.name);
        } else {
          this.debug('last top most is null.');
        }
        if (found) {
          this.debug('next top most is ' + this._topMost.name);
        } else {
          this.debug('next top most is null.');
        }

        if (this._topMost && this._topMost.setHierarchy &&
            this._topMost.setHierarchy(true)) {
          // Blur previous module only when current module is successfully
          // focused.
          lastTopMost && lastTopMost.setHierarchy &&
          lastTopMost.setHierarchy(false);

        }

        this._topMost && this._topMost.setHierarchy &&
        this._topMost.setHierarchy(true);
        this.publish('changed');
      } else {
        this.debug('top most is the same.', this._topMost ?
          this._topMost.name : 'NaN');
      }
    },

    dumpHierarchy: function() {
      this._ui_list.forEach(function(module, index) {
        this.debug(
          '[' + index + '] (' +
            this.PRIORITIES.indexOf(module.name) +')' +
            module.name +
          ', active state = ' + module.isActive());
      }, this);
    },

    focus: function(module) {
      if (!module) {
        this._topMost.setHierarchy(true);
      } else if (this._topMost === module) {
        module.setHierarchy(true);
      }
    },

    updateTopMostWindow: function() {
      var topMostWindow = this.getTopMostWindow();
      if (topMostWindow !== this._topMostWindow) {
        this._topMostWindow = topMostWindow;
        this.publish('topmostwindowchanged');
      }
    },

    handleEvent: function(evt) {
      this.debug(evt.type);
      switch (evt.type) {
        case 'windowopened':
        case 'windowclosed':
          this.updateTopMostWindow();
          break;
        case 'mozChromeEvent':
          if (!evt.detail ||
              evt.detail.type !== 'inputmethod-contextchange') {
            break;
          }
          /* falls through */
        case 'home':
        case 'holdhome':
        case 'launchactivity':
        case 'webapps-launch':
        case 'system-resize':
          this.broadcast(evt);
          break;
        default:
          this.debug('handling ' + evt.type);
          this.updateHierarchy();
          break;
      }
      return false;
    },

    /**
     * Broadcast hierarchy based event until it's blocked
     * @param  {DOMEvent} evt Event to be broadcast
     */
    broadcast: function(evt) {
      this._ui_list.some(function(ui, index) {
        // The last one will always catch the event if
        // there is nobody block it.
        // This rule is for task manager who is inactive but
        // needs to catch holdhome event as no one else
        // needs this event. If task manager's hierarchy is changed
        // we may need to change this rule as well.
        if ((ui.isActive() || index === this._ui_list.length - 1) &&
            ui.respondToHierarchyEvent) {
          // If the module wants to interrupt the event,
          // it should return false in the broadcast function.
          this.debug('handover ' + evt.type + ' to ' + ui.name);
          return (ui.respondToHierarchyEvent(evt) !== true);
        }
      }, this);
    },

    /**
     * This function is used for any UI module who wants to occupy the hierachy.
     */
    registerHierarchy: function(module) {
      if (!module.isActive) {
        return;
      }
      if (this._ui_list.indexOf(module) >= 0) {
        return;
      }

      this.debug(module.name + ' is registering the hierarchy');
      this._ui_list.push(module);
      this.sortHierarchy();
      window.addEventListener(module.EVENT_PREFIX + '-activating', this);
      window.addEventListener(module.EVENT_PREFIX + '-activated', this);
      window.addEventListener(module.EVENT_PREFIX + '-deactivating', this);
      window.addEventListener(module.EVENT_PREFIX + '-deactivated', this);
      this.updateHierarchy();
    },

    sortHierarchy: function() {
      this.debug('before sorting...');
      this.dumpHierarchy();
      var self = this;
      this._ui_list.sort(function(a, b) {
        return (self.PRIORITIES.indexOf(b.name) <
                self.PRIORITIES.indexOf(a.name));
      });
      this.debug('after sorting...');
      this.dumpHierarchy();
    },

    /**
     * Remove the module reference from the living UI list.
     */
    unregisterHierarchy: function(module) {
      var index = this._ui_list.indexOf(module);
      if (index < 0) {
        return;
      }
      this.debug(module.name + ' is unregistering the hierarchy');
      var removed = this._ui_list.splice(index, 1);
      this.sortHierarchy();
      this.debug(removed.name);
      window.removeEventListener(module.EVENT_PREFIX + '-activated', this);
      window.removeEventListener(module.EVENT_PREFIX + '-deactivated', this);
      this.updateHierarchy();
    }
  });
}());