michael@0: /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: Components.utils.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: Components.utils.import("resource://gre/modules/Services.jsm"); michael@0: michael@0: /** michael@0: * Constants michael@0: */ michael@0: michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: michael@0: // Stop updating jumplists after some idle time. michael@0: const IDLE_TIMEOUT_SECONDS = 5 * 60; michael@0: michael@0: // Prefs michael@0: const PREF_TASKBAR_BRANCH = "browser.taskbar.lists."; michael@0: const PREF_TASKBAR_ENABLED = "enabled"; michael@0: const PREF_TASKBAR_ITEMCOUNT = "maxListItemCount"; michael@0: const PREF_TASKBAR_FREQUENT = "frequent.enabled"; michael@0: const PREF_TASKBAR_RECENT = "recent.enabled"; michael@0: const PREF_TASKBAR_TASKS = "tasks.enabled"; michael@0: const PREF_TASKBAR_REFRESH = "refreshInSeconds"; michael@0: michael@0: // Hash keys for pendingStatements. michael@0: const LIST_TYPE = { michael@0: FREQUENT: 0 michael@0: , RECENT: 1 michael@0: } michael@0: michael@0: /** michael@0: * Exports michael@0: */ michael@0: michael@0: this.EXPORTED_SYMBOLS = [ michael@0: "WinTaskbarJumpList", michael@0: ]; michael@0: michael@0: /** michael@0: * Smart getters michael@0: */ michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "_prefs", function() { michael@0: return Services.prefs.getBranch(PREF_TASKBAR_BRANCH); michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "_stringBundle", function() { michael@0: return Services.strings michael@0: .createBundle("chrome://browser/locale/taskbar.properties"); michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "PlacesUtils", function() { michael@0: Components.utils.import("resource://gre/modules/PlacesUtils.jsm"); michael@0: return PlacesUtils; michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyGetter(this, "NetUtil", function() { michael@0: Components.utils.import("resource://gre/modules/NetUtil.jsm"); michael@0: return NetUtil; michael@0: }); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "_idle", michael@0: "@mozilla.org/widget/idleservice;1", michael@0: "nsIIdleService"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "_taskbarService", michael@0: "@mozilla.org/windows-taskbar;1", michael@0: "nsIWinTaskbar"); michael@0: michael@0: XPCOMUtils.defineLazyServiceGetter(this, "_winShellService", michael@0: "@mozilla.org/browser/shell-service;1", michael@0: "nsIWindowsShellService"); michael@0: michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", michael@0: "resource://gre/modules/PrivateBrowsingUtils.jsm"); michael@0: michael@0: /** michael@0: * Global functions michael@0: */ michael@0: michael@0: function _getString(name) { michael@0: return _stringBundle.GetStringFromName(name); michael@0: } michael@0: michael@0: ///////////////////////////////////////////////////////////////////////////// michael@0: // Task list configuration data object. michael@0: michael@0: var tasksCfg = [ michael@0: /** michael@0: * Task configuration options: title, description, args, iconIndex, open, close. michael@0: * michael@0: * title - Task title displayed in the list. (strings in the table are temp fillers.) michael@0: * description - Tooltip description on the list item. michael@0: * args - Command line args to invoke the task. michael@0: * iconIndex - Optional win icon index into the main application for the michael@0: * list item. michael@0: * open - Boolean indicates if the command should be visible after the browser opens. michael@0: * close - Boolean indicates if the command should be visible after the browser closes. michael@0: */ michael@0: // Open new tab michael@0: { michael@0: get title() _getString("taskbar.tasks.newTab.label"), michael@0: get description() _getString("taskbar.tasks.newTab.description"), michael@0: args: "-new-tab about:blank", michael@0: iconIndex: 3, // New window icon michael@0: open: true, michael@0: close: true, // The jump list already has an app launch icon, but michael@0: // we don't always update the list on shutdown. michael@0: // Thus true for consistency. michael@0: }, michael@0: michael@0: // Open new window michael@0: { michael@0: get title() _getString("taskbar.tasks.newWindow.label"), michael@0: get description() _getString("taskbar.tasks.newWindow.description"), michael@0: args: "-browser", michael@0: iconIndex: 2, // New tab icon michael@0: open: true, michael@0: close: true, // No point, but we don't always update the list on michael@0: // shutdown. Thus true for consistency. michael@0: }, michael@0: michael@0: // Open new private window michael@0: { michael@0: get title() _getString("taskbar.tasks.newPrivateWindow.label"), michael@0: get description() _getString("taskbar.tasks.newPrivateWindow.description"), michael@0: args: "-private-window", michael@0: iconIndex: 4, // Private browsing mode icon michael@0: open: true, michael@0: close: true, // No point, but we don't always update the list on michael@0: // shutdown. Thus true for consistency. michael@0: }, michael@0: ]; michael@0: michael@0: ///////////////////////////////////////////////////////////////////////////// michael@0: // Implementation michael@0: michael@0: this.WinTaskbarJumpList = michael@0: { michael@0: _builder: null, michael@0: _tasks: null, michael@0: _shuttingDown: false, michael@0: michael@0: /** michael@0: * Startup, shutdown, and update michael@0: */ michael@0: michael@0: startup: function WTBJL_startup() { michael@0: // exit if this isn't win7 or higher. michael@0: if (!this._initTaskbar()) michael@0: return; michael@0: michael@0: // Win shell shortcut maintenance. If we've gone through an update, michael@0: // this will update any pinned taskbar shortcuts. Not specific to michael@0: // jump lists, but this was a convienent place to call it. michael@0: try { michael@0: // dev builds may not have helper.exe, ignore failures. michael@0: this._shortcutMaintenance(); michael@0: } catch (ex) { michael@0: } michael@0: michael@0: // Store our task list config data michael@0: this._tasks = tasksCfg; michael@0: michael@0: // retrieve taskbar related prefs. michael@0: this._refreshPrefs(); michael@0: michael@0: // observer for private browsing and our prefs branch michael@0: this._initObs(); michael@0: michael@0: // jump list refresh timer michael@0: this._updateTimer(); michael@0: }, michael@0: michael@0: update: function WTBJL_update() { michael@0: // are we disabled via prefs? don't do anything! michael@0: if (!this._enabled) michael@0: return; michael@0: michael@0: // do what we came here to do, update the taskbar jumplist michael@0: this._buildList(); michael@0: }, michael@0: michael@0: _shutdown: function WTBJL__shutdown() { michael@0: this._shuttingDown = true; michael@0: michael@0: // Correctly handle a clear history on shutdown. If there are no michael@0: // entries be sure to empty all history lists. Luckily Places caches michael@0: // this value, so it's a pretty fast call. michael@0: if (!PlacesUtils.history.hasHistoryEntries) { michael@0: this.update(); michael@0: } michael@0: michael@0: this._free(); michael@0: }, michael@0: michael@0: _shortcutMaintenance: function WTBJL__maintenace() { michael@0: _winShellService.shortcutMaintenance(); michael@0: }, michael@0: michael@0: /** michael@0: * List building michael@0: * michael@0: * @note Async builders must add their mozIStoragePendingStatement to michael@0: * _pendingStatements object, using a different LIST_TYPE entry for michael@0: * each statement. Once finished they must remove it and call michael@0: * commitBuild(). When there will be no more _pendingStatements, michael@0: * commitBuild() will commit for real. michael@0: */ michael@0: michael@0: _pendingStatements: {}, michael@0: _hasPendingStatements: function WTBJL__hasPendingStatements() { michael@0: return Object.keys(this._pendingStatements).length > 0; michael@0: }, michael@0: michael@0: _buildList: function WTBJL__buildList() { michael@0: if (this._hasPendingStatements()) { michael@0: // We were requested to update the list while another update was in michael@0: // progress, this could happen at shutdown, idle or privatebrowsing. michael@0: // Abort the current list building. michael@0: for (let listType in this._pendingStatements) { michael@0: this._pendingStatements[listType].cancel(); michael@0: delete this._pendingStatements[listType]; michael@0: } michael@0: this._builder.abortListBuild(); michael@0: } michael@0: michael@0: // anything to build? michael@0: if (!this._showFrequent && !this._showRecent && !this._showTasks) { michael@0: // don't leave the last list hanging on the taskbar. michael@0: this._deleteActiveJumpList(); michael@0: return; michael@0: } michael@0: michael@0: if (!this._startBuild()) michael@0: return; michael@0: michael@0: if (this._showTasks) michael@0: this._buildTasks(); michael@0: michael@0: // Space for frequent items takes priority over recent. michael@0: if (this._showFrequent) michael@0: this._buildFrequent(); michael@0: michael@0: if (this._showRecent) michael@0: this._buildRecent(); michael@0: michael@0: this._commitBuild(); michael@0: }, michael@0: michael@0: /** michael@0: * Taskbar api wrappers michael@0: */ michael@0: michael@0: _startBuild: function WTBJL__startBuild() { michael@0: var removedItems = Cc["@mozilla.org/array;1"]. michael@0: createInstance(Ci.nsIMutableArray); michael@0: this._builder.abortListBuild(); michael@0: if (this._builder.initListBuild(removedItems)) { michael@0: // Prior to building, delete removed items from history. michael@0: this._clearHistory(removedItems); michael@0: return true; michael@0: } michael@0: return false; michael@0: }, michael@0: michael@0: _commitBuild: function WTBJL__commitBuild() { michael@0: if (!this._hasPendingStatements() && !this._builder.commitListBuild()) { michael@0: this._builder.abortListBuild(); michael@0: } michael@0: }, michael@0: michael@0: _buildTasks: function WTBJL__buildTasks() { michael@0: var items = Cc["@mozilla.org/array;1"]. michael@0: createInstance(Ci.nsIMutableArray); michael@0: this._tasks.forEach(function (task) { michael@0: if ((this._shuttingDown && !task.close) || (!this._shuttingDown && !task.open)) michael@0: return; michael@0: var item = this._getHandlerAppItem(task.title, task.description, michael@0: task.args, task.iconIndex, null); michael@0: items.appendElement(item, false); michael@0: }, this); michael@0: michael@0: if (items.length > 0) michael@0: this._builder.addListToBuild(this._builder.JUMPLIST_CATEGORY_TASKS, items); michael@0: }, michael@0: michael@0: _buildCustom: function WTBJL__buildCustom(title, items) { michael@0: if (items.length > 0) michael@0: this._builder.addListToBuild(this._builder.JUMPLIST_CATEGORY_CUSTOMLIST, items, title); michael@0: }, michael@0: michael@0: _buildFrequent: function WTBJL__buildFrequent() { michael@0: // If history is empty, just bail out. michael@0: if (!PlacesUtils.history.hasHistoryEntries) { michael@0: return; michael@0: } michael@0: michael@0: // Windows supports default frequent and recent lists, michael@0: // but those depend on internal windows visit tracking michael@0: // which we don't populate. So we build our own custom michael@0: // frequent and recent lists using our nav history data. michael@0: michael@0: var items = Cc["@mozilla.org/array;1"]. michael@0: createInstance(Ci.nsIMutableArray); michael@0: // track frequent items so that we don't add them to michael@0: // the recent list. michael@0: this._frequentHashList = []; michael@0: michael@0: this._pendingStatements[LIST_TYPE.FREQUENT] = this._getHistoryResults( michael@0: Ci.nsINavHistoryQueryOptions.SORT_BY_VISITCOUNT_DESCENDING, michael@0: this._maxItemCount, michael@0: function (aResult) { michael@0: if (!aResult) { michael@0: delete this._pendingStatements[LIST_TYPE.FREQUENT]; michael@0: // The are no more results, build the list. michael@0: this._buildCustom(_getString("taskbar.frequent.label"), items); michael@0: this._commitBuild(); michael@0: return; michael@0: } michael@0: michael@0: let title = aResult.title || aResult.uri; michael@0: let faviconPageUri = Services.io.newURI(aResult.uri, null, null); michael@0: let shortcut = this._getHandlerAppItem(title, title, aResult.uri, 1, michael@0: faviconPageUri); michael@0: items.appendElement(shortcut, false); michael@0: this._frequentHashList.push(aResult.uri); michael@0: }, michael@0: this michael@0: ); michael@0: }, michael@0: michael@0: _buildRecent: function WTBJL__buildRecent() { michael@0: // If history is empty, just bail out. michael@0: if (!PlacesUtils.history.hasHistoryEntries) { michael@0: return; michael@0: } michael@0: michael@0: var items = Cc["@mozilla.org/array;1"]. michael@0: createInstance(Ci.nsIMutableArray); michael@0: // Frequent items will be skipped, so we select a double amount of michael@0: // entries and stop fetching results at _maxItemCount. michael@0: var count = 0; michael@0: michael@0: this._pendingStatements[LIST_TYPE.RECENT] = this._getHistoryResults( michael@0: Ci.nsINavHistoryQueryOptions.SORT_BY_DATE_DESCENDING, michael@0: this._maxItemCount * 2, michael@0: function (aResult) { michael@0: if (!aResult) { michael@0: // The are no more results, build the list. michael@0: this._buildCustom(_getString("taskbar.recent.label"), items); michael@0: delete this._pendingStatements[LIST_TYPE.RECENT]; michael@0: this._commitBuild(); michael@0: return; michael@0: } michael@0: michael@0: if (count >= this._maxItemCount) { michael@0: return; michael@0: } michael@0: michael@0: // Do not add items to recent that have already been added to frequent. michael@0: if (this._frequentHashList && michael@0: this._frequentHashList.indexOf(aResult.uri) != -1) { michael@0: return; michael@0: } michael@0: michael@0: let title = aResult.title || aResult.uri; michael@0: let faviconPageUri = Services.io.newURI(aResult.uri, null, null); michael@0: let shortcut = this._getHandlerAppItem(title, title, aResult.uri, 1, michael@0: faviconPageUri); michael@0: items.appendElement(shortcut, false); michael@0: count++; michael@0: }, michael@0: this michael@0: ); michael@0: }, michael@0: michael@0: _deleteActiveJumpList: function WTBJL__deleteAJL() { michael@0: this._builder.deleteActiveList(); michael@0: }, michael@0: michael@0: /** michael@0: * Jump list item creation helpers michael@0: */ michael@0: michael@0: _getHandlerAppItem: function WTBJL__getHandlerAppItem(name, description, michael@0: args, iconIndex, michael@0: faviconPageUri) { michael@0: var file = Services.dirsvc.get("XREExeF", Ci.nsILocalFile); michael@0: michael@0: var handlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"]. michael@0: createInstance(Ci.nsILocalHandlerApp); michael@0: handlerApp.executable = file; michael@0: // handlers default to the leaf name if a name is not specified michael@0: if (name && name.length != 0) michael@0: handlerApp.name = name; michael@0: handlerApp.detailedDescription = description; michael@0: handlerApp.appendParameter(args); michael@0: michael@0: var item = Cc["@mozilla.org/windows-jumplistshortcut;1"]. michael@0: createInstance(Ci.nsIJumpListShortcut); michael@0: item.app = handlerApp; michael@0: item.iconIndex = iconIndex; michael@0: item.faviconPageUri = faviconPageUri; michael@0: return item; michael@0: }, michael@0: michael@0: _getSeparatorItem: function WTBJL__getSeparatorItem() { michael@0: var item = Cc["@mozilla.org/windows-jumplistseparator;1"]. michael@0: createInstance(Ci.nsIJumpListSeparator); michael@0: return item; michael@0: }, michael@0: michael@0: /** michael@0: * Nav history helpers michael@0: */ michael@0: michael@0: _getHistoryResults: michael@0: function WTBLJL__getHistoryResults(aSortingMode, aLimit, aCallback, aScope) { michael@0: var options = PlacesUtils.history.getNewQueryOptions(); michael@0: options.maxResults = aLimit; michael@0: options.sortingMode = aSortingMode; michael@0: var query = PlacesUtils.history.getNewQuery(); michael@0: michael@0: // Return the pending statement to the caller, to allow cancelation. michael@0: return PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) michael@0: .asyncExecuteLegacyQueries([query], 1, options, { michael@0: handleResult: function (aResultSet) { michael@0: for (let row; (row = aResultSet.getNextRow());) { michael@0: try { michael@0: aCallback.call(aScope, michael@0: { uri: row.getResultByIndex(1) michael@0: , title: row.getResultByIndex(2) michael@0: }); michael@0: } catch (e) {} michael@0: } michael@0: }, michael@0: handleError: function (aError) { michael@0: Components.utils.reportError( michael@0: "Async execution error (" + aError.result + "): " + aError.message); michael@0: }, michael@0: handleCompletion: function (aReason) { michael@0: aCallback.call(WinTaskbarJumpList, null); michael@0: }, michael@0: }); michael@0: }, michael@0: michael@0: _clearHistory: function WTBJL__clearHistory(items) { michael@0: if (!items) michael@0: return; michael@0: var URIsToRemove = []; michael@0: var e = items.enumerate(); michael@0: while (e.hasMoreElements()) { michael@0: let oldItem = e.getNext().QueryInterface(Ci.nsIJumpListShortcut); michael@0: if (oldItem) { michael@0: try { // in case we get a bad uri michael@0: let uriSpec = oldItem.app.getParameter(0); michael@0: URIsToRemove.push(NetUtil.newURI(uriSpec)); michael@0: } catch (err) { } michael@0: } michael@0: } michael@0: if (URIsToRemove.length > 0) { michael@0: PlacesUtils.bhistory.removePages(URIsToRemove, URIsToRemove.length, true); michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Prefs utilities michael@0: */ michael@0: michael@0: _refreshPrefs: function WTBJL__refreshPrefs() { michael@0: this._enabled = _prefs.getBoolPref(PREF_TASKBAR_ENABLED); michael@0: this._showFrequent = _prefs.getBoolPref(PREF_TASKBAR_FREQUENT); michael@0: this._showRecent = _prefs.getBoolPref(PREF_TASKBAR_RECENT); michael@0: this._showTasks = _prefs.getBoolPref(PREF_TASKBAR_TASKS); michael@0: this._maxItemCount = _prefs.getIntPref(PREF_TASKBAR_ITEMCOUNT); michael@0: }, michael@0: michael@0: /** michael@0: * Init and shutdown utilities michael@0: */ michael@0: michael@0: _initTaskbar: function WTBJL__initTaskbar() { michael@0: this._builder = _taskbarService.createJumpListBuilder(); michael@0: if (!this._builder || !this._builder.available) michael@0: return false; michael@0: michael@0: return true; michael@0: }, michael@0: michael@0: _initObs: function WTBJL__initObs() { michael@0: // If the browser is closed while in private browsing mode, the "exit" michael@0: // notification is fired on quit-application-granted. michael@0: // History cleanup can happen at profile-change-teardown. michael@0: Services.obs.addObserver(this, "profile-before-change", false); michael@0: Services.obs.addObserver(this, "browser:purge-session-history", false); michael@0: _prefs.addObserver("", this, false); michael@0: }, michael@0: michael@0: _freeObs: function WTBJL__freeObs() { michael@0: Services.obs.removeObserver(this, "profile-before-change"); michael@0: Services.obs.removeObserver(this, "browser:purge-session-history"); michael@0: _prefs.removeObserver("", this); michael@0: }, michael@0: michael@0: _updateTimer: function WTBJL__updateTimer() { michael@0: if (this._enabled && !this._shuttingDown && !this._timer) { michael@0: this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); michael@0: this._timer.initWithCallback(this, michael@0: _prefs.getIntPref(PREF_TASKBAR_REFRESH)*1000, michael@0: this._timer.TYPE_REPEATING_SLACK); michael@0: } michael@0: else if ((!this._enabled || this._shuttingDown) && this._timer) { michael@0: this._timer.cancel(); michael@0: delete this._timer; michael@0: } michael@0: }, michael@0: michael@0: _hasIdleObserver: false, michael@0: _updateIdleObserver: function WTBJL__updateIdleObserver() { michael@0: if (this._enabled && !this._shuttingDown && !this._hasIdleObserver) { michael@0: _idle.addIdleObserver(this, IDLE_TIMEOUT_SECONDS); michael@0: this._hasIdleObserver = true; michael@0: } michael@0: else if ((!this._enabled || this._shuttingDown) && this._hasIdleObserver) { michael@0: _idle.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS); michael@0: this._hasIdleObserver = false; michael@0: } michael@0: }, michael@0: michael@0: _free: function WTBJL__free() { michael@0: this._freeObs(); michael@0: this._updateTimer(); michael@0: this._updateIdleObserver(); michael@0: delete this._builder; michael@0: }, michael@0: michael@0: /** michael@0: * Notification handlers michael@0: */ michael@0: michael@0: notify: function WTBJL_notify(aTimer) { michael@0: // Add idle observer on the first notification so it doesn't hit startup. michael@0: this._updateIdleObserver(); michael@0: this.update(); michael@0: }, michael@0: michael@0: observe: function WTBJL_observe(aSubject, aTopic, aData) { michael@0: switch (aTopic) { michael@0: case "nsPref:changed": michael@0: if (this._enabled == true && !_prefs.getBoolPref(PREF_TASKBAR_ENABLED)) michael@0: this._deleteActiveJumpList(); michael@0: this._refreshPrefs(); michael@0: this._updateTimer(); michael@0: this._updateIdleObserver(); michael@0: this.update(); michael@0: break; michael@0: michael@0: case "profile-before-change": michael@0: this._shutdown(); michael@0: break; michael@0: michael@0: case "browser:purge-session-history": michael@0: this.update(); michael@0: break; michael@0: case "idle": michael@0: if (this._timer) { michael@0: this._timer.cancel(); michael@0: delete this._timer; michael@0: } michael@0: break; michael@0: michael@0: case "active": michael@0: this._updateTimer(); michael@0: break; michael@0: } michael@0: }, michael@0: };