// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

/**
 * @fileoverview PageListView implementation.
 * PageListView manages page list, dot list, switcher buttons and handles apps
 * pages callbacks from backend.
 *
 * Note that you need to have AppLauncherHandler in your WebUI to use this code.
 */

cr.define('ntp', function() {
  'use strict';

  /**
   * Creates a PageListView object.
   * @constructor
   * @extends {Object}
   */
  function PageListView() {
  }

  PageListView.prototype = {
    /**
     * The CardSlider object to use for changing app pages.
     * @type {CardSlider|undefined}
     */
    cardSlider: undefined,

    /**
     * The frame div for this.cardSlider.
     * @type {!Element|undefined}
     */
    sliderFrame: undefined,

    /**
     * The 'page-list' element.
     * @type {!Element|undefined}
     */
    pageList: undefined,

    /**
     * A list of all 'tile-page' elements.
     * @type {!NodeList|undefined}
     */
    tilePages: undefined,

    /**
     * A list of all 'apps-page' elements.
     * @type {!NodeList|undefined}
     */
    appsPages: undefined,

    /**
     * The Suggestions page.
     * @type {!Element|undefined}
     */
    suggestionsPage: undefined,

    /**
     * The Most Visited page.
     * @type {!Element|undefined}
     */
    mostVisitedPage: undefined,

    /**
     * The 'dots-list' element.
     * @type {!Element|undefined}
     */
    dotList: undefined,

    /**
     * The left and right paging buttons.
     * @type {!Element|undefined}
     */
    pageSwitcherStart: undefined,
    pageSwitcherEnd: undefined,

    /**
     * The 'trash' element.  Note that technically this is unnecessary,
     * JavaScript creates the object for us based on the id.  But I don't want
     * to rely on the ID being the same, and JSCompiler doesn't know about it.
     * @type {!Element|undefined}
     */
    trash: undefined,

    /**
     * The type of page that is currently shown. The value is a numerical ID.
     * @type {number}
     */
    shownPage: 0,

    /**
     * The index of the page that is currently shown, within the page type.
     * For example if the third Apps page is showing, this will be 2.
     * @type {number}
     */
    shownPageIndex: 0,

    /**
     * EventTracker for managing event listeners for page events.
     * @type {!EventTracker}
     */
    eventTracker: new EventTracker,

    /**
     * If non-null, this is the ID of the app to highlight to the user the next
     * time getAppsCallback runs. "Highlight" in this case means to switch to
     * the page and run the new tile animation.
     * @type {?string}
     */
    highlightAppId: null,

    /**
     * Initializes page list view.
     * @param {!Element} pageList A DIV element to host all pages.
     * @param {!Element} dotList An UL element to host nav dots. Each dot
     *     represents a page.
     * @param {!Element} cardSliderFrame The card slider frame that hosts
     *     pageList and switcher buttons.
     * @param {!Element|undefined} opt_trash Optional trash element.
     * @param {!Element|undefined} opt_pageSwitcherStart Optional start page
     *     switcher button.
     * @param {!Element|undefined} opt_pageSwitcherEnd Optional end page
     *     switcher button.
     */
    initialize: function(pageList, dotList, cardSliderFrame, opt_trash,
                         opt_pageSwitcherStart, opt_pageSwitcherEnd) {
      this.pageList = pageList;

      this.dotList = dotList;
      cr.ui.decorate(this.dotList, ntp.DotList);

      this.trash = opt_trash;
      if (this.trash)
        new ntp.Trash(this.trash);

      this.pageSwitcherStart = opt_pageSwitcherStart;
      if (this.pageSwitcherStart)
        ntp.initializePageSwitcher(this.pageSwitcherStart);

      this.pageSwitcherEnd = opt_pageSwitcherEnd;
      if (this.pageSwitcherEnd)
        ntp.initializePageSwitcher(this.pageSwitcherEnd);

      this.shownPage = loadTimeData.getInteger('shown_page_type');
      this.shownPageIndex = loadTimeData.getInteger('shown_page_index');

      if (loadTimeData.getBoolean('showApps')) {
        // Request data on the apps so we can fill them in.
        // Note that this is kicked off asynchronously.  'getAppsCallback' will
        // be invoked at some point after this function returns.
        chrome.send('getApps');
      } else {
        // No apps page.
        if (this.shownPage == loadTimeData.getInteger('apps_page_id')) {
          this.setShownPage_(
              loadTimeData.getInteger('most_visited_page_id'), 0);
        }

        document.body.classList.add('bare-minimum');
      }

      document.addEventListener('keydown', this.onDocKeyDown_.bind(this));

      this.tilePages = this.pageList.getElementsByClassName('tile-page');
      this.appsPages = this.pageList.getElementsByClassName('apps-page');

      // Initialize the cardSlider without any cards at the moment.
      this.sliderFrame = cardSliderFrame;
      this.cardSlider = new cr.ui.CardSlider(this.sliderFrame, this.pageList,
          this.sliderFrame.offsetWidth);

      // Prevent touch events from triggering any sort of native scrolling if
      // there are multiple cards in the slider frame.
      var cardSlider = this.cardSlider;
      cardSliderFrame.addEventListener('touchmove', function(e) {
        if (cardSlider.cardCount <= 1)
          return;
        e.preventDefault();
      }, true);

      // Handle mousewheel events anywhere in the card slider, so that wheel
      // events on the page switchers will still scroll the page.
      // This listener must be added before the card slider is initialized,
      // because it needs to be called before the card slider's handler.
      cardSliderFrame.addEventListener('mousewheel', function(e) {
        if (cardSlider.currentCardValue.handleMouseWheel(e)) {
          e.preventDefault();  // Prevent default scroll behavior.
          e.stopImmediatePropagation();  // Prevent horizontal card flipping.
        }
      });

      this.cardSlider.initialize(
          loadTimeData.getBoolean('isSwipeTrackingFromScrollEventsEnabled'));

      // Handle events from the card slider.
      this.pageList.addEventListener('cardSlider:card_changed',
                                     this.onCardChanged_.bind(this));
      this.pageList.addEventListener('cardSlider:card_added',
                                     this.onCardAdded_.bind(this));
      this.pageList.addEventListener('cardSlider:card_removed',
                                     this.onCardRemoved_.bind(this));

      // Ensure the slider is resized appropriately with the window.
      window.addEventListener('resize', this.onWindowResize_.bind(this));

      // Update apps when online state changes.
      window.addEventListener('online',
          this.updateOfflineEnabledApps_.bind(this));
      window.addEventListener('offline',
          this.updateOfflineEnabledApps_.bind(this));
    },

    /**
     * Appends a tile page.
     *
     * @param {TilePage} page The page element.
     * @param {string} title The title of the tile page.
     * @param {boolean} titleIsEditable If true, the title can be changed.
     * @param {TilePage} opt_refNode Optional reference node to insert in front
     *     of.
     * When opt_refNode is falsey, |page| will just be appended to the end of
     * the page list.
     */
    appendTilePage: function(page, title, titleIsEditable, opt_refNode) {
      if (opt_refNode) {
        var refIndex = this.getTilePageIndex(opt_refNode);
        this.cardSlider.addCardAtIndex(page, refIndex);
      } else {
        this.cardSlider.appendCard(page);
      }

      // Remember special MostVisitedPage.
      if (typeof ntp.MostVisitedPage != 'undefined' &&
          page instanceof ntp.MostVisitedPage) {
        assert(this.tilePages.length == 1,
               'MostVisitedPage should be added as first tile page');
        this.mostVisitedPage = page;
      }

      if (typeof ntp.SuggestionsPage != 'undefined' &&
          page instanceof ntp.SuggestionsPage) {
        this.suggestionsPage = page;
      }

      // If we're appending an AppsPage and it's a temporary page, animate it.
      var animate = page instanceof ntp.AppsPage &&
                    page.classList.contains('temporary');
      // Make a deep copy of the dot template to add a new one.
      var newDot = new ntp.NavDot(page, title, titleIsEditable, animate);
      page.navigationDot = newDot;
      this.dotList.insertBefore(newDot,
                                opt_refNode ? opt_refNode.navigationDot : null);
      // Set a tab index on the first dot.
      if (this.dotList.dots.length == 1)
        newDot.tabIndex = 3;

      this.eventTracker.add(page, 'pagelayout', this.onPageLayout_.bind(this));
    },

    /**
     * Called by chrome when an app has changed positions.
     * @param {Object} appData The data for the app. This contains page and
     *     position indices.
     */
    appMoved: function(appData) {
      assert(loadTimeData.getBoolean('showApps'));

      var app = $(appData.id);
      assert(app, 'trying to move an app that doesn\'t exist');
      app.remove(false);

      this.appsPages[appData.page_index].insertApp(appData, false);
    },

    /**
     * Called by chrome when an existing app has been disabled or
     * removed/uninstalled from chrome.
     * @param {Object} appData A data structure full of relevant information for
     *     the app.
     * @param {boolean} isUninstall True if the app is being uninstalled;
     *     false if the app is being disabled.
     * @param {boolean} fromPage True if the removal was from the current page.
     */
    appRemoved: function(appData, isUninstall, fromPage) {
      assert(loadTimeData.getBoolean('showApps'));

      var app = $(appData.id);
      assert(app, 'trying to remove an app that doesn\'t exist');

      if (!isUninstall)
        app.replaceAppData(appData);
      else
        app.remove(!!fromPage);
    },

    /**
     * @return {boolean} If the page is still starting up.
     * @private
     */
    isStartingUp_: function() {
      return document.documentElement.classList.contains('starting-up');
    },

    /**
     * Tracks whether apps have been loaded at least once.
     * @type {boolean}
     * @private
     */
    appsLoaded_: false,

    /**
     * Callback invoked by chrome with the apps available.
     *
     * Note that calls to this function can occur at any time, not just in
     * response to a getApps request. For example, when a user
     * installs/uninstalls an app on another synchronized devices.
     * @param {Object} data An object with all the data on available
     *        applications.
     */
    getAppsCallback: function(data) {
      assert(loadTimeData.getBoolean('showApps'));

      var startTime = Date.now();

      // Remember this to select the correct card when done rebuilding.
      var prevCurrentCard = this.cardSlider.currentCard;

      // Make removal of pages and dots as quick as possible with less DOM
      // operations, reflows, or repaints. We set currentCard = 0 and remove
      // from the end to not encounter any auto-magic card selections in the
      // process and we hide the card slider throughout.
      this.cardSlider.currentCard = 0;

      // Clear any existing apps pages and dots.
      // TODO(rbyers): It might be nice to preserve animation of dots after an
      // uninstall. Could we re-use the existing page and dot elements?  It
      // seems unfortunate to have Chrome send us the entire apps list after an
      // uninstall.
      while (this.appsPages.length > 0)
        this.removeTilePageAndDot_(this.appsPages[this.appsPages.length - 1]);

      // Get the array of apps and add any special synthesized entries
      var apps = data.apps;

      // Get a list of page names
      var pageNames = data.appPageNames;

      function stringListIsEmpty(list) {
        for (var i = 0; i < list.length; i++) {
          if (list[i])
            return false;
        }
        return true;
      }

      // Sort by launch ordinal
      apps.sort(function(a, b) {
        return a.app_launch_ordinal > b.app_launch_ordinal ? 1 :
          a.app_launch_ordinal < b.app_launch_ordinal ? -1 : 0;
      });

      // An app to animate (in case it was just installed).
      var highlightApp;

      // If there are any pages after the apps, add new pages before them.
      var lastAppsPage = (this.appsPages.length > 0) ?
          this.appsPages[this.appsPages.length - 1] : null;
      var lastAppsPageIndex = (lastAppsPage != null) ?
          Array.prototype.indexOf.call(this.tilePages, lastAppsPage) : -1;
      var nextPageAfterApps = lastAppsPageIndex != -1 ?
          this.tilePages[lastAppsPageIndex + 1] : null;

      // Add the apps, creating pages as necessary
      for (var i = 0; i < apps.length; i++) {
        var app = apps[i];
        var pageIndex = app.page_index || 0;
        while (pageIndex >= this.appsPages.length) {
          var pageName = loadTimeData.getString('appDefaultPageName');
          if (this.appsPages.length < pageNames.length)
            pageName = pageNames[this.appsPages.length];

          var origPageCount = this.appsPages.length;
          this.appendTilePage(new ntp.AppsPage(), pageName, true,
                              nextPageAfterApps);
          // Confirm that appsPages is a live object, updated when a new page is
          // added (otherwise we'd have an infinite loop)
          assert(this.appsPages.length == origPageCount + 1,
                 'expected new page');
        }

        if (app.id == this.highlightAppId)
          highlightApp = app;
        else
          this.appsPages[pageIndex].insertApp(app, false);
      }

      this.cardSlider.currentCard = prevCurrentCard;

      if (highlightApp)
        this.appAdded(highlightApp, true);

      logEvent('apps.layout: ' + (Date.now() - startTime));

      // Tell the slider about the pages and mark the current page.
      this.updateSliderCards();
      this.cardSlider.currentCardValue.navigationDot.classList.add('selected');

      if (!this.appsLoaded_) {
        this.appsLoaded_ = true;
        cr.dispatchSimpleEvent(document, 'sectionready', true, true);
      }
      this.updateAppLauncherPromoHiddenState_();
    },

    /**
     * Called by chrome when a new app has been added to chrome or has been
     * enabled if previously disabled.
     * @param {Object} appData A data structure full of relevant information for
     *     the app.
     * @param {boolean=} opt_highlight Whether the app about to be added should
     *     be highlighted.
     */
    appAdded: function(appData, opt_highlight) {
      assert(loadTimeData.getBoolean('showApps'));

      if (appData.id == this.highlightAppId) {
        opt_highlight = true;
        this.highlightAppId = null;
      }

      var pageIndex = appData.page_index || 0;

      if (pageIndex >= this.appsPages.length) {
        while (pageIndex >= this.appsPages.length) {
          this.appendTilePage(new ntp.AppsPage(),
                              loadTimeData.getString('appDefaultPageName'),
                              true);
        }
        this.updateSliderCards();
      }

      var page = this.appsPages[pageIndex];
      var app = $(appData.id);
      if (app) {
        app.replaceAppData(appData);
      } else if (opt_highlight) {
        page.insertAndHighlightApp(appData);
        this.setShownPage_(loadTimeData.getInteger('apps_page_id'),
                           appData.page_index);
      } else {
        page.insertApp(appData, false);
      }
    },

    /**
     * Callback invoked by chrome whenever an app preference changes.
     * @param {Object} data An object with all the data on available
     *     applications.
     */
    appsPrefChangedCallback: function(data) {
      assert(loadTimeData.getBoolean('showApps'));

      for (var i = 0; i < data.apps.length; ++i) {
        $(data.apps[i].id).appData = data.apps[i];
      }

      // Set the App dot names. Skip the first dot (Most Visited).
      var dots = this.dotList.getElementsByClassName('dot');
      var start = this.mostVisitedPage ? 1 : 0;
      for (var i = start; i < dots.length; ++i) {
        dots[i].displayTitle = data.appPageNames[i - start] || '';
      }
    },

    /**
     * Callback invoked by chrome whenever the app launcher promo pref changes.
     * @param {boolean} show Identifies if we should show or hide the promo.
     */
    appLauncherPromoPrefChangeCallback: function(show) {
      loadTimeData.overrideValues({showAppLauncherPromo: show});
      this.updateAppLauncherPromoHiddenState_();
    },

    /**
     * Updates the hidden state of the app launcher promo based on the page
     * shown and load data content.
     */
    updateAppLauncherPromoHiddenState_: function() {
      $('app-launcher-promo').hidden =
          !loadTimeData.getBoolean('showAppLauncherPromo') ||
          this.shownPage != loadTimeData.getInteger('apps_page_id');
    },

    /**
     * Invoked whenever the pages in apps-page-list have changed so that
     * the Slider knows about the new elements.
     */
    updateSliderCards: function() {
      var pageNo = Math.max(0, Math.min(this.cardSlider.currentCard,
                                        this.tilePages.length - 1));
      this.cardSlider.setCards(Array.prototype.slice.call(this.tilePages),
                               pageNo);
      // The shownPage property was potentially saved from a previous webui that
      // didn't have the same set of pages as the current one. So we cascade
      // from suggestions, to most visited and then to apps because we can have
      // an page with apps only (e.g., chrome://apps) or one with only the most
      // visited, but not one with only suggestions. And we alwayd default to
      // most visited first when previously shown page is not availabel anymore.
      // If most visited isn't there either, we go to apps.
      if (this.shownPage == loadTimeData.getInteger('suggestions_page_id')) {
        if (this.suggestionsPage)
          this.cardSlider.selectCardByValue(this.suggestionsPage);
        else
          this.shownPage = loadTimeData.getInteger('most_visited_page_id');
      }
      if (this.shownPage == loadTimeData.getInteger('most_visited_page_id')) {
        if (this.mostVisitedPage)
          this.cardSlider.selectCardByValue(this.mostVisitedPage);
        else
          this.shownPage = loadTimeData.getInteger('apps_page_id');
      }
      if (this.shownPage == loadTimeData.getInteger('apps_page_id') &&
          loadTimeData.getBoolean('showApps')) {
        this.cardSlider.selectCardByValue(
            this.appsPages[Math.min(this.shownPageIndex,
                                    this.appsPages.length - 1)]);
      } else if (this.mostVisitedPage) {
        this.shownPage = loadTimeData.getInteger('most_visited_page_id');
        this.cardSlider.selectCardByValue(this.mostVisitedPage);
      }
    },

    /**
     * Called whenever tiles should be re-arranging themselves out of the way
     * of a moving or insert tile.
     */
    enterRearrangeMode: function() {
      if (loadTimeData.getBoolean('showApps')) {
        var tempPage = new ntp.AppsPage();
        tempPage.classList.add('temporary');
        var pageName = loadTimeData.getString('appDefaultPageName');
        this.appendTilePage(tempPage, pageName, true);
      }

      if (ntp.getCurrentlyDraggingTile().firstChild.canBeRemoved()) {
        $('footer').classList.add('showing-trash-mode');
        $('footer-menu-container').style.minWidth = $('trash').offsetWidth -
            $('chrome-web-store-link').offsetWidth + 'px';
      }

      document.documentElement.classList.add('dragging-mode');
    },

    /**
     * Invoked whenever some app is released
     */
    leaveRearrangeMode: function() {
      var tempPage = document.querySelector('.tile-page.temporary');
      if (tempPage) {
        var dot = tempPage.navigationDot;
        if (!tempPage.tileCount &&
            tempPage != this.cardSlider.currentCardValue) {
          this.removeTilePageAndDot_(tempPage, true);
        } else {
          tempPage.classList.remove('temporary');
          this.saveAppPageName(tempPage,
                               loadTimeData.getString('appDefaultPageName'));
        }
      }

      $('footer').classList.remove('showing-trash-mode');
      $('footer-menu-container').style.minWidth = '';
      document.documentElement.classList.remove('dragging-mode');
    },

    /**
     * Callback for the 'pagelayout' event.
     * @param {Event} e The event.
     */
    onPageLayout_: function(e) {
      if (Array.prototype.indexOf.call(this.tilePages, e.currentTarget) !=
          this.cardSlider.currentCard) {
        return;
      }

      this.updatePageSwitchers();
    },

    /**
     * Adjusts the size and position of the page switchers according to the
     * layout of the current card, and updates the aria-label attributes of
     * the page switchers.
     */
    updatePageSwitchers: function() {
      if (!this.pageSwitcherStart || !this.pageSwitcherEnd)
        return;

      var page = this.cardSlider.currentCardValue;

      this.pageSwitcherStart.hidden = !page ||
          (this.cardSlider.currentCard == 0);
      this.pageSwitcherEnd.hidden = !page ||
          (this.cardSlider.currentCard == this.cardSlider.cardCount - 1);

      if (!page)
        return;

      var pageSwitcherLeft = isRTL() ? this.pageSwitcherEnd :
                                       this.pageSwitcherStart;
      var pageSwitcherRight = isRTL() ? this.pageSwitcherStart :
                                        this.pageSwitcherEnd;
      var scrollbarWidth = page.scrollbarWidth;
      pageSwitcherLeft.style.width =
          (page.sideMargin + 13) + 'px';
      pageSwitcherLeft.style.left = '0';
      pageSwitcherRight.style.width =
          (page.sideMargin - scrollbarWidth + 13) + 'px';
      pageSwitcherRight.style.right = scrollbarWidth + 'px';

      var offsetTop = page.querySelector('.tile-page-content').offsetTop + 'px';
      pageSwitcherLeft.style.top = offsetTop;
      pageSwitcherRight.style.top = offsetTop;
      pageSwitcherLeft.style.paddingBottom = offsetTop;
      pageSwitcherRight.style.paddingBottom = offsetTop;

      // Update the aria-label attributes of the two page switchers.
      this.pageSwitcherStart.updateButtonAccessibleLabel(this.dotList.dots);
      this.pageSwitcherEnd.updateButtonAccessibleLabel(this.dotList.dots);
    },

    /**
     * Returns the index of the given apps page.
     * @param {AppsPage} page The AppsPage we wish to find.
     * @return {number} The index of |page| or -1 if it is not in the
     *    collection.
     */
    getAppsPageIndex: function(page) {
      return Array.prototype.indexOf.call(this.appsPages, page);
    },

    /**
     * Handler for cardSlider:card_changed events from this.cardSlider.
     * @param {Event} e The cardSlider:card_changed event.
     * @private
     */
    onCardChanged_: function(e) {
      var page = e.cardSlider.currentCardValue;

      // Don't change shownPage until startup is done (and page changes actually
      // reflect user actions).
      if (!this.isStartingUp_()) {
        if (page.classList.contains('apps-page')) {
          this.setShownPage_(loadTimeData.getInteger('apps_page_id'),
                             this.getAppsPageIndex(page));
        } else if (page.classList.contains('most-visited-page')) {
          this.setShownPage_(
              loadTimeData.getInteger('most_visited_page_id'), 0);
        } else if (page.classList.contains('suggestions-page')) {
          this.setShownPage_(loadTimeData.getInteger('suggestions_page_id'), 0);
        } else {
          console.error('unknown page selected');
        }
      }

      // Update the active dot
      var curDot = this.dotList.getElementsByClassName('selected')[0];
      if (curDot)
        curDot.classList.remove('selected');
      page.navigationDot.classList.add('selected');
      this.updatePageSwitchers();
    },

    /**
     * Saves/updates the newly selected page to open when first loading the NTP.
     * @type {number} shownPage The new shown page type.
     * @type {number} shownPageIndex The new shown page index.
     * @private
     */
    setShownPage_: function(shownPage, shownPageIndex) {
      assert(shownPageIndex >= 0);
      this.shownPage = shownPage;
      this.shownPageIndex = shownPageIndex;
      chrome.send('pageSelected', [this.shownPage, this.shownPageIndex]);
      this.updateAppLauncherPromoHiddenState_();
    },

    /**
     * Listen for card additions to update the page switchers or the current
     * card accordingly.
     * @param {Event} e A card removed or added event.
     */
    onCardAdded_: function(e) {
      // When the second arg passed to insertBefore is falsey, it acts just like
      // appendChild.
      this.pageList.insertBefore(e.addedCard, this.tilePages[e.addedIndex]);
      this.onCardAddedOrRemoved_();
    },

    /**
     * Listen for card removals to update the page switchers or the current card
     * accordingly.
     * @param {Event} e A card removed or added event.
     */
    onCardRemoved_: function(e) {
      e.removedCard.parentNode.removeChild(e.removedCard);
      this.onCardAddedOrRemoved_();
    },

    /**
     * Called when a card is removed or added.
     * @private
     */
    onCardAddedOrRemoved_: function() {
      if (this.isStartingUp_())
        return;

      // Without repositioning there were issues - http://crbug.com/133457.
      this.cardSlider.repositionFrame();
      this.updatePageSwitchers();
    },

    /**
     * Save the name of an apps page.
     * Store the apps page name into the preferences store.
     * @param {AppsPage} appsPage The app page for which we wish to save.
     * @param {string} name The name of the page.
     */
    saveAppPageName: function(appPage, name) {
      var index = this.getAppsPageIndex(appPage);
      assert(index != -1);
      chrome.send('saveAppPageName', [name, index]);
    },

    /**
     * Window resize handler.
     * @private
     */
    onWindowResize_: function(e) {
      this.cardSlider.resize(this.sliderFrame.offsetWidth);
      this.updatePageSwitchers();
    },

    /**
     * Listener for offline status change events. Updates apps that are
     * not offline-enabled to be grayscale if the browser is offline.
     * @private
     */
    updateOfflineEnabledApps_: function() {
      var apps = document.querySelectorAll('.app');
      for (var i = 0; i < apps.length; ++i) {
        if (apps[i].appData.enabled && !apps[i].appData.offline_enabled) {
          apps[i].setIcon();
          apps[i].loadIcon();
        }
      }
    },

    /**
     * Handler for key events on the page. Ctrl-Arrow will switch the visible
     * page.
     * @param {Event} e The KeyboardEvent.
     * @private
     */
    onDocKeyDown_: function(e) {
      if (!e.ctrlKey || e.altKey || e.metaKey || e.shiftKey)
        return;

      var direction = 0;
      if (e.keyIdentifier == 'Left')
        direction = -1;
      else if (e.keyIdentifier == 'Right')
        direction = 1;
      else
        return;

      var cardIndex =
          (this.cardSlider.currentCard + direction +
           this.cardSlider.cardCount) % this.cardSlider.cardCount;
      this.cardSlider.selectCard(cardIndex, true);

      e.stopPropagation();
    },

    /**
     * Returns the index of a given tile page.
     * @param {TilePage} page The TilePage we wish to find.
     * @return {number} The index of |page| or -1 if it is not in the
     *    collection.
     */
    getTilePageIndex: function(page) {
      return Array.prototype.indexOf.call(this.tilePages, page);
    },

    /**
     * Removes a page and navigation dot (if the navdot exists).
     * @param {TilePage} page The page to be removed.
     * @param {boolean=} opt_animate If the removal should be animated.
     */
    removeTilePageAndDot_: function(page, opt_animate) {
      if (page.navigationDot)
        page.navigationDot.remove(opt_animate);
      this.cardSlider.removeCard(page);
    },
  };

  return {
    PageListView: PageListView
  };
});