/* -*- 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/. */ const {XPCOMUtils} = ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); /** * Constants */ // 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 */ var 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://navigator/locale/taskbar.properties"); }); XPCOMUtils.defineLazyServiceGetter(this, "_idle", "@mozilla.org/widget/idleservice;1", "nsIIdleService"); XPCOMUtils.defineLazyServiceGetter(this, "_taskbarService", "@mozilla.org/windows-taskbar;1", "nsIWinTaskbar"); ChromeUtils.defineModuleGetter(this, "PlacesUtils", "resource://gre/modules/PlacesUtils.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() { return _getString("taskbar.tasks.newTab.label"); }, get description() { return _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() { return _getString("taskbar.tasks.newWindow.label"); }, get description() { return _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 private window { get title() { return _getString("taskbar.tasks.newPrivate.label"); }, get description() { return _getString("taskbar.tasks.newPrivate.description"); }, args: "-private", 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() { return _getString("taskbar.tasks.mailWindow.label"); }, get description() { return _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() { return _getString("taskbar.tasks.composeMessage.label"); }, get description() { return _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() { return _getString("taskbar.tasks.openAddressBook.label"); }, get description() { return _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() { return _getString("taskbar.tasks.openEditor.label"); }, get description() { return _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; // 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(); }, /** * 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()) { return; } this._builder.commitListBuild(succeed => { if (!succeed) { 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, null); items.appendElement(item); }, 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 faviconPageUri = Services.io.newURI(aResult.uri); let shortcut = this._getHandlerAppItem(title, title, aResult.uri, 2, faviconPageUri); items.appendElement(shortcut); 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.includes(aResult.uri)) { return; } let title = aResult.title || aResult.uri; let faviconPageUri = Services.io.newURI(aResult.uri); let shortcut = this._getHandlerAppItem(title, title, aResult.uri, 2, faviconPageUri); items.appendElement(shortcut); count++; }, this ); }, _deleteActiveJumpList: function WTBJL__deleteAJL() { this._builder.deleteActiveList(); }, /** * Jump list item creation helpers */ _getHandlerAppItem: function WTBJL__getHandlerAppItem(name, description, args, iconIndex, faviconPageUri) { var file = Services.dirsvc.get("XREExeF", Ci.nsIFile); 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 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; 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) { Cu.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(Services.io.newURI(uriSpec)); } catch (err) { } } } if (URIsToRemove.length > 0) { PlacesUtils.history.remove(URIsToRemove).catch(Cu.reportError); } }, /** * 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"); Services.obs.addObserver(this, "browser:purge-session-history"); _prefs.addObserver("", this); }, _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(); Services.tm.idleDispatchToMainThread(() => { 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(); Services.tm.idleDispatchToMainThread(() => { 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 "active": this._updateTimer(); break; } }, };