/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 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 */ this.EXPORTED_SYMBOLS = [ "WinTaskbarJumpList", ]; /** * Smart getters */ XPCOMUtils.defineLazyGetter(this, "_prefs", function() { return Services.prefs.getBranch(PREF_TASKBAR_BRANCH); }); XPCOMUtils.defineLazyGetter(this, "_stringBundle", function() { return Services.strings .createBundle("chrome://browser/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"); XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm"); /** * 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: 3, // New window 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: 2, // New tab icon open: true, close: true, // No point, but we don't always update the list on // shutdown. Thus true for consistency. }, // Open new private window { get title() _getString("taskbar.tasks.newPrivateWindow.label"), get description() _getString("taskbar.tasks.newPrivateWindow.description"), args: "-private-window", iconIndex: 4, // Private browsing mode icon open: true, close: true, // No point, but we don't always update the list on // shutdown. Thus true for consistency. }, ]; ///////////////////////////////////////////////////////////////////////////// // Implementation this.WinTaskbarJumpList = { _builder: null, _tasks: null, _shuttingDown: false, /** * Startup, shutdown, and update */ startup: function() { // Exit if there's something wrong with getting the taskbar service. 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 { // 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 private browsing and our prefs branch this._initObs(); // jump list refresh timer this._updateTimer(); }, update: function() { // 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() { 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() { _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() { return Object.keys(this._pendingStatements).length > 0; }, _buildList: function() { if (this._hasPendingStatements()) { // We were requested to update the list while another update was in // progress, this could happen at shutdown, idle or privatebrowsing. // 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() { 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() { if (!this._hasPendingStatements() && !this._builder.commitListBuild()) { this._builder.abortListBuild(); } }, _buildTasks: function() { 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, null); items.appendElement(item, false); }, this); if (items.length > 0) { this._builder.addListToBuild(this._builder.JUMPLIST_CATEGORY_TASKS, items); } }, _buildCustom: function(title, items) { if (items.length > 0) { this._builder.addListToBuild(this._builder.JUMPLIST_CATEGORY_CUSTOMLIST, items, title); } }, _buildFrequent: function() { // 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 faviconPageUri = Services.io.newURI(aResult.uri, null, null); let shortcut = this._getHandlerAppItem(title, title, aResult.uri, 1, faviconPageUri); items.appendElement(shortcut, false); this._frequentHashList.push(aResult.uri); }, this); }, _buildRecent: function() { // 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 faviconPageUri = Services.io.newURI(aResult.uri, null, null); let shortcut = this._getHandlerAppItem(title, title, aResult.uri, 1, faviconPageUri); items.appendElement(shortcut, false); count++; }, this); }, _deleteActiveJumpList: function() { this._builder.deleteActiveList(); }, /** * Jump list item creation helpers */ _getHandlerAppItem: function(name, description, args, iconIndex, faviconPageUri) { var file = Services.dirsvc.get("XREExeF", Ci.nsILocalFile); 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 = iconIndex; item.faviconPageUri = faviconPageUri; return item; }, _getSeparatorItem: function() { var item = Cc["@mozilla.org/windows-jumplistseparator;1"] .createInstance(Ci.nsIJumpListSeparator); return item; }, /** * Nav history helpers */ _getHistoryResults: function(aSortingMode, aLimit, aCallback, aScope) { var options = PlacesUtils.history.getNewQueryOptions(); options.maxResults = aLimit; options.sortingMode = aSortingMode; 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(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() { 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() { this._builder = _taskbarService.createJumpListBuilder(); if (!this._builder || !this._builder.available) return false; return true; }, _initObs: function() { // 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() { Services.obs.removeObserver(this, "profile-before-change"); Services.obs.removeObserver(this, "browser:purge-session-history"); _prefs.removeObserver("", this); }, _updateTimer: function() { 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() { 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() { this._freeObs(); this._updateTimer(); this._updateIdleObserver(); delete this._builder; }, /** * Notification handlers */ notify: function(aTimer) { // Add idle observer on the first notification so it doesn't hit startup. this._updateIdleObserver(); this.update(); }, observe: function(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; } }, };