suite/modules/WindowsJumpLists.jsm
author Mark Banner <bugzilla@standard8.plus.com>
Tue, 08 Nov 2011 20:22:54 +0000
changeset 9343 20ddb394e7e8710f84aa26ccdc2fb53bf044efb4
parent 8722 3b67c67a2ab0528db52c476ecddb6887c5fab093
child 10430 24c90a00c902107d04a052ca915ed049b21ec260
permissions -rw-r--r--
Added tag BETA_BASE_20111108 for changeset 96f9be89d243 a=betamerge DONTBUILD CLOSED TREE

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is mozilla.org code.
 *
 * The Initial Developer of the Original Code is
 * the Mozilla Foundation.
 * Portions created by the Initial Developer are Copyright (C) 2009
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *   Jim Mathies <jmathies@mozilla.com> (Original author)
 *   Marco Bonardo <mak77@bonardo.net>
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
Components.utils.import("resource://gre/modules/Services.jsm");

/**
 * Constants
 */

const Cc = Components.classes;
const Ci = Components.interfaces;

// Stop updating jumplists after some idle time.
const IDLE_TIMEOUT_SECONDS = 5 * 60;

// Prefs
const PREF_TASKBAR_BRANCH    = "browser.taskbar.lists.";
const PREF_TASKBAR_ENABLED   = "enabled";
const PREF_TASKBAR_ITEMCOUNT = "maxListItemCount";
const PREF_TASKBAR_FREQUENT  = "frequent.enabled";
const PREF_TASKBAR_RECENT    = "recent.enabled";
const PREF_TASKBAR_TASKS     = "tasks.enabled";
const PREF_TASKBAR_REFRESH   = "refreshInSeconds";

// Hash keys for pendingStatements.
const LIST_TYPE = {
  FREQUENT: 0
, RECENT: 1
}

/**
 * Exports
 */

let EXPORTED_SYMBOLS = [
  "WinTaskbarJumpList",
];

/**
 * Smart getters
 */

XPCOMUtils.defineLazyGetter(this, "_prefs", function() {
  return Services.prefs.getBranch(PREF_TASKBAR_BRANCH)
                       .QueryInterface(Ci.nsIPrefBranch2);
});

XPCOMUtils.defineLazyGetter(this, "_stringBundle", function() {
  return Services.strings
                 .createBundle("chrome://navigator/locale/taskbar.properties");
});

XPCOMUtils.defineLazyGetter(this, "PlacesUtils", function() {
  Components.utils.import("resource://gre/modules/PlacesUtils.jsm");
  return PlacesUtils;
});

XPCOMUtils.defineLazyGetter(this, "NetUtil", function() {
  Components.utils.import("resource://gre/modules/NetUtil.jsm");
  return NetUtil;
});

XPCOMUtils.defineLazyServiceGetter(this, "_idle",
                                   "@mozilla.org/widget/idleservice;1",
                                   "nsIIdleService");

XPCOMUtils.defineLazyServiceGetter(this, "_taskbarService",
                                   "@mozilla.org/windows-taskbar;1",
                                   "nsIWinTaskbar");

XPCOMUtils.defineLazyServiceGetter(this, "_winShellService",
                                   "@mozilla.org/browser/shell-service;1",
                                   "nsIWindowsShellService");

/**
 * Global functions
 */

function _getString(name) {
  return _stringBundle.GetStringFromName(name);
}

/////////////////////////////////////////////////////////////////////////////
// Task list configuration data object.

var tasksCfg = [
  /**
   * Task configuration options: title, description, args, iconIndex, open, close.
   *
   * title       - Task title displayed in the list. (strings in the table are temp fillers.)
   * description - Tooltip description on the list item.
   * args        - Command line args to invoke the task.
   * iconIndex   - Optional win icon index into the main application for the
   *               list item.
   * open        - Boolean indicates if the command should be visible after the browser opens.
   * close       - Boolean indicates if the command should be visible after the browser closes.
   */
  // Open new tab
  {
    get title()       _getString("taskbar.tasks.newTab.label"),
    get description() _getString("taskbar.tasks.newTab.description"),
    args:             "-new-tab about:blank",
    iconIndex:        0, // SeaMonkey app icon
    open:             true,
    close:            true, // The jump list already has an app launch icon, but
                            // we don't always update the list on shutdown.
                            // Thus true for consistency.
  },

  // Open new window
  {
    get title()       _getString("taskbar.tasks.newWindow.label"),
    get description() _getString("taskbar.tasks.newWindow.description"),
    args:             "-browser",
    iconIndex:        0, // SeaMonkey app icon
    open:             true,
    close:            true, // No point, but we don't always update the list on
                            //  shutdown.  Thus true for consistency.
  },

  // Open mailnews
  {
    get title()       _getString("taskbar.tasks.mailWindow.label"),
    get description() _getString("taskbar.tasks.mailWindow.description"),
    args:             "-mail",
    iconIndex:        0, // SeaMonkey app icon
    open:             true,
    close:            true, // No point, but we don't always update the list on
                            //  shutdown.  Thus true for consistency.
  },

  // Compose Message
  {
    get title()       _getString("taskbar.tasks.composeMessage.label"),
    get description() _getString("taskbar.tasks.composeMessage.description"),
    args:             "-compose",
    iconIndex:        0, // SeaMonkey app icon
    open:             true,
    close:            true, // No point, but we don't always update the list on
                            //  shutdown.  Thus true for consistency.
  },

  // Address Book
  {
    get title()       _getString("taskbar.tasks.openAddressBook.label"),
    get description() _getString("taskbar.tasks.openAddressBook.description"),
    args:             "-addressbook",
    iconIndex:        0, // SeaMonkey app icon
    open:             true,
    close:            true, // No point, but we don't always update the list on
                            //  shutdown.  Thus true for consistency.
  },

  // Composer
  {
    get title()       _getString("taskbar.tasks.openEditor.label"),
    get description() _getString("taskbar.tasks.openEditor.description"),
    args:             "-edit",
    iconIndex:        0, // SeaMonkey app icon
    open:             true,
    close:            true, // No point, but we don't always update the list on
                            //  shutdown.  Thus true for consistency.
  },
];

/////////////////////////////////////////////////////////////////////////////
// Implementation

var WinTaskbarJumpList =
{
  _builder: null,
  _tasks: null,
  _shuttingDown: false,

  /**
   * Startup, shutdown, and update
   */ 

  startup: function WTBJL_startup() {
    // exit if this isn't win7 or higher.
    if (!this._initTaskbar())
      return;

    // Win shell shortcut maintenance. If we've gone through an update,
    // this will update any pinned taskbar shortcuts. Not specific to
    // jump lists, but this was a convienent place to call it. 
    try {
      // dev builds may not have helper.exe, ignore failures.
      this._shortcutMaintenance();
    } catch (ex) {
    }

    // Store our task list config data
    this._tasks = tasksCfg;

    // retrieve taskbar related prefs.
    this._refreshPrefs();

    // observer for our prefs branch
    this._initObs();

    // jump list refresh timer
    this._updateTimer();
  },

  update: function WTBJL_update() {
    // are we disabled via prefs? don't do anything!
    if (!this._enabled)
      return;

    // do what we came here to do, update the taskbar jumplist
    this._buildList();
  },

  _shutdown: function WTBJL__shutdown() {
    this._shuttingDown = true;

    // Correctly handle a clear history on shutdown.  If there are no
    // entries be sure to empty all history lists.  Luckily Places caches
    // this value, so it's a pretty fast call.
    if (!PlacesUtils.history.hasHistoryEntries) {
      this.update();
    }

    this._free();
  },

  _shortcutMaintenance: function WTBJL__maintenance() {
    _winShellService.shortcutMaintenance();
  },

  /**
   * List building
   *
   * @note Async builders must add their mozIStoragePendingStatement to
   *       _pendingStatements object, using a different LIST_TYPE entry for
   *       each statement. Once finished they must remove it and call
   *       commitBuild().  When there will be no more _pendingStatements,
   *       commitBuild() will commit for real.
   */

  _pendingStatements: {},
  _hasPendingStatements: function WTBJL__hasPendingStatements() {
    return Object.keys(this._pendingStatements).length > 0;
  },

  _buildList: function WTBJL__buildList() {
    if (this._hasPendingStatements()) {
      // We were requested to update the list while another update was in
      // progress, this could happen at shutdown or idle.
      // Abort the current list building.
      for (let listType in this._pendingStatements) {
        this._pendingStatements[listType].cancel();
        delete this._pendingStatements[listType];
      }
      this._builder.abortListBuild();
    }

    // anything to build?
    if (!this._showFrequent && !this._showRecent && !this._showTasks) {
      // don't leave the last list hanging on the taskbar.
      this._deleteActiveJumpList();
      return;
    }

    if (!this._startBuild())
      return;

    if (this._showTasks)
      this._buildTasks();

    // Space for frequent items takes priority over recent.
    if (this._showFrequent)
      this._buildFrequent();

    if (this._showRecent)
      this._buildRecent();

    this._commitBuild();
  },

  /**
   * Taskbar api wrappers
   */ 

  _startBuild: function WTBJL__startBuild() {
    var removedItems = Cc["@mozilla.org/array;1"].
                       createInstance(Ci.nsIMutableArray);
    this._builder.abortListBuild();
    if (this._builder.initListBuild(removedItems)) { 
      // Prior to building, delete removed items from history.
      this._clearHistory(removedItems);
      return true;
    }
    return false;
  },

  _commitBuild: function WTBJL__commitBuild() {
    if (!this._hasPendingStatements() && !this._builder.commitListBuild()) {
      this._builder.abortListBuild();
    }
  },

  _buildTasks: function WTBJL__buildTasks() {
    var items = Cc["@mozilla.org/array;1"].
                createInstance(Ci.nsIMutableArray);
    this._tasks.forEach(function (task) {
      if ((this._shuttingDown && !task.close) || (!this._shuttingDown && !task.open))
        return;
      var item = this._getHandlerAppItem(task.title, task.description,
                                         task.args, task.iconIndex);
      items.appendElement(item, false);
    }, this);
    
    if (items.length > 0)
      this._builder.addListToBuild(this._builder.JUMPLIST_CATEGORY_TASKS, items);
  },

  _buildCustom: function WTBJL__buildCustom(title, items) {
    if (items.length > 0)
      this._builder.addListToBuild(this._builder.JUMPLIST_CATEGORY_CUSTOMLIST, items, title);
  },

  _buildFrequent: function WTBJL__buildFrequent() {
    // If history is empty, just bail out.
    if (!PlacesUtils.history.hasHistoryEntries) {
      return;
    }

    // Windows supports default frequent and recent lists,
    // but those depend on internal windows visit tracking
    // which we don't populate. So we build our own custom
    // frequent and recent lists using our nav history data.

    var items = Cc["@mozilla.org/array;1"].
                createInstance(Ci.nsIMutableArray);
    // track frequent items so that we don't add them to
    // the recent list.
    this._frequentHashList = [];

    this._pendingStatements[LIST_TYPE.FREQUENT] = this._getHistoryResults(
      Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING,
      this._maxItemCount,
      function (aResult) {
        if (!aResult) {
          delete this._pendingStatements[LIST_TYPE.FREQUENT];
          // The are no more results, build the list.
          this._buildCustom(_getString("taskbar.frequent.label"), items);
          this._commitBuild();
          return;
        }

        let title = aResult.title || aResult.uri;
        let shortcut = this._getHandlerAppItem(title, title, aResult.uri, 1);
        items.appendElement(shortcut, false);
        this._frequentHashList.push(aResult.uri);
      },
      this
    );
  },

  _buildRecent: function WTBJL__buildRecent() {
    // If history is empty, just bail out.
    if (!PlacesUtils.history.hasHistoryEntries) {
      return;
    }

    var items = Cc["@mozilla.org/array;1"].
                createInstance(Ci.nsIMutableArray);
    // Frequent items will be skipped, so we select a double amount of
    // entries and stop fetching results at _maxItemCount.
    var count = 0;

    this._pendingStatements[LIST_TYPE.RECENT] = this._getHistoryResults(
      Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING,
      this._maxItemCount * 2,
      function (aResult) {
        if (!aResult) {
          // The are no more results, build the list.
          this._buildCustom(_getString("taskbar.recent.label"), items);
          delete this._pendingStatements[LIST_TYPE.RECENT];
          this._commitBuild();
          return;
        }

        if (count >= this._maxItemCount) {
          return;
        }

        // Do not add items to recent that have already been added to frequent.
        if (this._frequentHashList &&
            this._frequentHashList.indexOf(aResult.uri) != -1) {
          return;
        }

        let title = aResult.title || aResult.uri;
        let shortcut = this._getHandlerAppItem(title, title, aResult.uri, 1);
        items.appendElement(shortcut, false);
        count++;
      },
      this
    );
  },

  _deleteActiveJumpList: function WTBJL__deleteAJL() {
    this._builder.deleteActiveList();
  },

  /**
   * Jump list item creation helpers
   */

  _getHandlerAppItem: function WTBJL__getHandlerAppItem(name, description, args, icon) {
    var file = Services.dirsvc.get("XCurProcD", Ci.nsILocalFile);

    // XXX where can we grab this from in the build? Do we need to?
    file.append("seamonkey.exe");

    var handlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"].
                     createInstance(Ci.nsILocalHandlerApp);
    handlerApp.executable = file;
    // handlers default to the leaf name if a name is not specified
    if (name && name.length != 0)
      handlerApp.name = name;
    handlerApp.detailedDescription = description;
    handlerApp.appendParameter(args);

    var item = Cc["@mozilla.org/windows-jumplistshortcut;1"].
               createInstance(Ci.nsIJumpListShortcut);
    item.app = handlerApp;
    item.iconIndex = icon;
    return item;
  },

  _getSeparatorItem: function WTBJL__getSeparatorItem() {
    var item = Cc["@mozilla.org/windows-jumplistseparator;1"].
               createInstance(Ci.nsIJumpListSeparator);
    return item;
  },

  /**
   * Nav history helpers
   */

  _getHistoryResults:
  function WTBLJL__getHistoryResults(aSortingMode, aLimit, aCallback, aScope) {
    var options = PlacesUtils.history.getNewQueryOptions();
    options.maxResults = aLimit;
    options.sortingMode = aSortingMode;
    // We don't want source redirects for these queries.
    options.redirectsMode = Ci.nsINavHistoryQueryOptions.REDIRECTS_MODE_TARGET;
    var query = PlacesUtils.history.getNewQuery();

    // Return the pending statement to the caller, to allow cancelation.
    return PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase)
                              .asyncExecuteLegacyQueries([query], 1, options, {
      handleResult: function (aResultSet) {
        for (let row; (row = aResultSet.getNextRow());) {
          try {
            aCallback.call(aScope,
                           { uri: row.getResultByIndex(1)
                           , title: row.getResultByIndex(2)
                           });
          } catch (e) {}
        }
      },
      handleError: function (aError) {
        Components.utils.reportError(
          "Async execution error (" + aError.result + "): " + aError.message);
      },
      handleCompletion: function (aReason) {
        aCallback.call(WinTaskbarJumpList, null);
      },
    });
  },

  _clearHistory: function WTBJL__clearHistory(items) {
    if (!items)
      return;
    var URIsToRemove = [];
    var e = items.enumerate();
    while (e.hasMoreElements()) {
      let oldItem = e.getNext().QueryInterface(Ci.nsIJumpListShortcut);
      if (oldItem) {
        try { // in case we get a bad uri
          let uriSpec = oldItem.app.getParameter(0);
          URIsToRemove.push(NetUtil.newURI(uriSpec));
        } catch (err) { }
      }
    }
    if (URIsToRemove.length > 0) {
      PlacesUtils.bhistory.removePages(URIsToRemove, URIsToRemove.length, true);
    }
  },

  /**
   * Prefs utilities
   */ 

  _refreshPrefs: function WTBJL__refreshPrefs() {
    this._enabled = _prefs.getBoolPref(PREF_TASKBAR_ENABLED);
    this._showFrequent = _prefs.getBoolPref(PREF_TASKBAR_FREQUENT);
    this._showRecent = _prefs.getBoolPref(PREF_TASKBAR_RECENT);
    this._showTasks = _prefs.getBoolPref(PREF_TASKBAR_TASKS);
    this._maxItemCount = _prefs.getIntPref(PREF_TASKBAR_ITEMCOUNT);
  },

  /**
   * Init and shutdown utilities
   */ 

  _initTaskbar: function WTBJL__initTaskbar() {
    this._builder = _taskbarService.createJumpListBuilder();
    if (!this._builder || !this._builder.available)
      return false;

    return true;
  },

  _initObs: function WTBJL__initObs() {
    // If the browser is closed while in private browsing mode, the "exit"
    // notification is fired on quit-application-granted.
    // History cleanup can happen at profile-change-teardown.
    Services.obs.addObserver(this, "profile-before-change", false);
    Services.obs.addObserver(this, "browser:purge-session-history", false);
    _prefs.addObserver("", this, false);
  },
 
  _freeObs: function WTBJL__freeObs() {
    Services.obs.removeObserver(this, "profile-before-change");
    Services.obs.removeObserver(this, "browser:purge-session-history");
    _prefs.removeObserver("", this);
  },

  _updateTimer: function WTBJL__updateTimer() {
    if (this._enabled && !this._shuttingDown && !this._timer) {
      this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
      this._timer.initWithCallback(this,
                                   _prefs.getIntPref(PREF_TASKBAR_REFRESH)*1000,
                                   this._timer.TYPE_REPEATING_SLACK);
    }
    else if ((!this._enabled || this._shuttingDown) && this._timer) {
      this._timer.cancel();
      delete this._timer;
    }
  },

  _hasIdleObserver: false,
  _updateIdleObserver: function WTBJL__updateIdleObserver() {
    if (this._enabled && !this._shuttingDown && !this._hasIdleObserver) {
      _idle.addIdleObserver(this, IDLE_TIMEOUT_SECONDS);
      this._hasIdleObserver = true;
    }
    else if ((!this._enabled || this._shuttingDown) && this._hasIdleObserver) {
      _idle.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS);
      this._hasIdleObserver = false;
    }
  },

  _free: function WTBJL__free() {
    this._freeObs();
    this._updateTimer();
    this._updateIdleObserver();
    delete this._builder;
  },

  /**
   * Notification handlers
   */

  notify: function WTBJL_notify(aTimer) {
    // Add idle observer on the first notification so it doesn't hit startup.
    this._updateIdleObserver();
    this.update();
  },

  observe: function WTBJL_observe(aSubject, aTopic, aData) {
    switch (aTopic) {
      case "nsPref:changed":
        if (this._enabled == true && !_prefs.getBoolPref(PREF_TASKBAR_ENABLED))
          this._deleteActiveJumpList();
        this._refreshPrefs();
        this._updateTimer();
        this._updateIdleObserver();
        this.update();
      break;

      case "profile-before-change":
        this._shutdown();
      break;

      case "browser:purge-session-history":
        this.update();
      break;

      case "idle":
        if (this._timer) {
          this._timer.cancel();
          delete this._timer;
        }
      break;

      case "back":
        this._updateTimer();
      break;
    }
  },
};