diff options
Diffstat (limited to 'browser/modules/WindowsJumpLists.jsm')
-rw-r--r-- | browser/modules/WindowsJumpLists.jsm | 657 |
1 files changed, 657 insertions, 0 deletions
diff --git a/browser/modules/WindowsJumpLists.jsm b/browser/modules/WindowsJumpLists.jsm new file mode 100644 index 0000000000..885955c92c --- /dev/null +++ b/browser/modules/WindowsJumpLists.jsm @@ -0,0 +1,657 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 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.importESModule( + "resource://gre/modules/XPCOMUtils.sys.mjs" +); + +// 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"]; + +const lazy = {}; + +/** + * Smart getters + */ + +XPCOMUtils.defineLazyGetter(lazy, "_prefs", function() { + return Services.prefs.getBranch(PREF_TASKBAR_BRANCH); +}); + +XPCOMUtils.defineLazyGetter(lazy, "_stringBundle", function() { + return Services.strings.createBundle( + "chrome://browser/locale/taskbar.properties" + ); +}); + +XPCOMUtils.defineLazyServiceGetter( + lazy, + "_idle", + "@mozilla.org/widget/useridleservice;1", + "nsIUserIdleService" +); +XPCOMUtils.defineLazyServiceGetter( + lazy, + "_taskbarService", + "@mozilla.org/windows-taskbar;1", + "nsIWinTaskbar" +); + +ChromeUtils.defineESModuleGetters(lazy, { + PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs", + PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", +}); + +/** + * Global functions + */ + +function _getString(name) { + return lazy._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: 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() { + return _getString("taskbar.tasks.newWindow.label"); + }, + get description() { + return _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 +let privateWindowTask = { + get title() { + return _getString("taskbar.tasks.newPrivateWindow.label"); + }, + get description() { + return _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 + +var Builder = class { + constructor(builder) { + this._builder = builder; + this._tasks = null; + this._pendingStatements = {}; + this._shuttingDown = false; + // These are ultimately controlled by prefs, so we disable + // everything until is read from there + this._showTasks = false; + this._showFrequent = false; + this._showRecent = false; + this._maxItemCount = 0; + } + + refreshPrefs(showTasks, showFrequent, showRecent, maxItemCount) { + this._showTasks = showTasks; + this._showFrequent = showFrequent; + this._showRecent = showRecent; + this._maxItemCount = maxItemCount; + } + + updateShutdownState(shuttingDown) { + this._shuttingDown = shuttingDown; + } + + delete() { + delete this._builder; + } + + /** + * 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. + */ + + _hasPendingStatements() { + return !!Object.keys(this._pendingStatements).length; + } + + async buildList() { + if ( + (this._showFrequent || this._showRecent) && + 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; + } + + await this._startBuild(); + + 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 + */ + + async _startBuild() { + this._builder.abortListBuild(); + let URIsToRemove = await this._builder.initListBuild(); + if (URIsToRemove.length) { + // Prior to building, delete removed items from history. + this._clearHistory(URIsToRemove); + } + } + + _commitBuild() { + if ( + (this._showFrequent || this._showRecent) && + this._hasPendingStatements() + ) { + return; + } + + this._builder.commitListBuild(succeed => { + if (!succeed) { + this._builder.abortListBuild(); + } + }); + } + + _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) { + this._builder.addListToBuild( + this._builder.JUMPLIST_CATEGORY_TASKS, + items + ); + } + } + + _buildCustom(title, items) { + if (items.length) { + this._builder.addListToBuild( + this._builder.JUMPLIST_CATEGORY_CUSTOMLIST, + items, + title + ); + } + } + + _buildFrequent() { + // 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, + 1, + faviconPageUri + ); + items.appendElement(shortcut); + this._frequentHashList.push(aResult.uri); + }, + this + ); + } + + _buildRecent() { + 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, + 1, + faviconPageUri + ); + items.appendElement(shortcut); + count++; + }, + this + ); + } + + _deleteActiveJumpList() { + this._builder.deleteActiveList(); + } + + /** + * Jump list item creation helpers + */ + + _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) { + 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; + } + + /** + * Nav history helpers + */ + + _getHistoryResults(aSortingMode, aLimit, aCallback, aScope) { + var options = lazy.PlacesUtils.history.getNewQueryOptions(); + options.maxResults = aLimit; + options.sortingMode = aSortingMode; + var query = lazy.PlacesUtils.history.getNewQuery(); + + // Return the pending statement to the caller, to allow cancelation. + return lazy.PlacesUtils.history.asyncExecuteLegacyQuery(query, options, { + handleResult(aResultSet) { + for (let row; (row = aResultSet.getNextRow()); ) { + try { + aCallback.call(aScope, { + uri: row.getResultByIndex(1), + title: row.getResultByIndex(2), + }); + } catch (e) {} + } + }, + handleError(aError) { + console.error( + "Async execution error (", + aError.result, + "): ", + aError.message + ); + }, + handleCompletion(aReason) { + aCallback.call(aScope, null); + }, + }); + } + + _clearHistory(uriSpecsToRemove) { + let URIsToRemove = uriSpecsToRemove + .map(spec => { + try { + // in case we get a bad uri + return Services.io.newURI(spec); + } catch (e) { + return null; + } + }) + .filter(uri => !!uri); + + if (URIsToRemove.length) { + lazy.PlacesUtils.history.remove(URIsToRemove).catch(console.error); + } + } +}; + +var WinTaskbarJumpList = { + // We build two separate jump lists -- one for the regular Firefox icon + // and one for the Private Browsing icon + _builder: null, + _pbBuilder: null, + _builtPb: false, + _shuttingDown: false, + + /** + * Startup, shutdown, and update + */ + + startup: function WTBJL_startup() { + // exit if this isn't win7 or higher. + if (!this._initTaskbar()) { + return; + } + + if (lazy.PrivateBrowsingUtils.enabled) { + tasksCfg.push(privateWindowTask); + } + // Store our task list config data + this._builder._tasks = tasksCfg; + this._pbBuilder._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 WTBJL_update() { + // are we disabled via prefs? don't do anything! + if (!this._enabled) { + return; + } + + // we only need to do this once, but we do it here + // to avoid main thread io on startup + if (!this._builtPb) { + this._pbBuilder.buildList(); + this._builtPb = true; + } + + // do what we came here to do, update the taskbar jumplist + this._builder.buildList(); + }, + + _shutdown: function WTBJL__shutdown() { + this._builder.updateShutdownState(true); + this._pbBuilder.updateShutdownState(true); + this._shuttingDown = true; + this._free(); + }, + + /** + * Prefs utilities + */ + + _refreshPrefs: function WTBJL__refreshPrefs() { + this._enabled = lazy._prefs.getBoolPref(PREF_TASKBAR_ENABLED); + var showTasks = lazy._prefs.getBoolPref(PREF_TASKBAR_TASKS); + this._builder.refreshPrefs( + showTasks, + lazy._prefs.getBoolPref(PREF_TASKBAR_FREQUENT), + lazy._prefs.getBoolPref(PREF_TASKBAR_RECENT), + lazy._prefs.getIntPref(PREF_TASKBAR_ITEMCOUNT) + ); + // showTasks is the only relevant pref for the Private Browsing Jump List + // the others are are related to frequent/recent entries, which are + // explicitly disabled for it + this._pbBuilder.refreshPrefs(showTasks, false, false, 0); + }, + + /** + * Init and shutdown utilities + */ + + _initTaskbar: function WTBJL__initTaskbar() { + var builder = lazy._taskbarService.createJumpListBuilder(false); + var pbBuilder = lazy._taskbarService.createJumpListBuilder(true); + if (!builder || !builder.available || !pbBuilder || !pbBuilder.available) { + return false; + } + + this._builder = new Builder(builder, true, true, true); + this._pbBuilder = new Builder(pbBuilder, true, false, 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"); + lazy._prefs.addObserver("", this); + this._placesObserver = new PlacesWeakCallbackWrapper( + this.update.bind(this) + ); + lazy.PlacesUtils.observers.addListener( + ["history-cleared"], + this._placesObserver + ); + }, + + _freeObs: function WTBJL__freeObs() { + Services.obs.removeObserver(this, "profile-before-change"); + Services.obs.removeObserver(this, "browser:purge-session-history"); + lazy._prefs.removeObserver("", this); + if (this._placesObserver) { + lazy.PlacesUtils.observers.removeListener( + ["history-cleared"], + this._placesObserver + ); + } + }, + + _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, + lazy._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) { + lazy._idle.addIdleObserver(this, IDLE_TIMEOUT_SECONDS); + this._hasIdleObserver = true; + } else if ( + (!this._enabled || this._shuttingDown) && + this._hasIdleObserver + ) { + lazy._idle.removeIdleObserver(this, IDLE_TIMEOUT_SECONDS); + this._hasIdleObserver = false; + } + }, + + _free: function WTBJL__free() { + this._freeObs(); + this._updateTimer(); + this._updateIdleObserver(); + this._builder.delete(); + this._pbBuilder.delete(); + }, + + 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 && !lazy._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; + } + }, +}; |