Source code

Revision control

Copy as Markdown

Other Tools

/* -*- 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/. */
import { XPCOMUtils } from "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_LEGACY_BACKEND = "legacyBackend";
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
*/
const lazy = {};
/**
* Smart getters
*/
ChromeUtils.defineLazyGetter(lazy, "_prefs", function () {
return Services.prefs.getBranch(PREF_TASKBAR_BRANCH);
});
ChromeUtils.defineLazyGetter(lazy, "_stringBundle", function () {
return Services.strings.createBundle(
);
});
XPCOMUtils.defineLazyServiceGetter(
lazy,
"_idle",
"@mozilla.org/widget/useridleservice;1",
"nsIUserIdleService"
);
XPCOMUtils.defineLazyServiceGetter(
lazy,
"_taskbarService",
"@mozilla.org/windows-taskbar;1",
"nsIWinTaskbar"
);
ChromeUtils.defineESModuleGetters(lazy, {
});
/**
* 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;
this._isBuilding = false;
}
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;
}
/**
* Constructs the tasks and recent history items to display in the JumpList,
* and then sends those lists to the nsIJumpListBuilder to be written.
*
* @returns {Promise<undefined>}
* The Promise resolves once the JumpList has been written, and any
* items that the user remove from the recent history list have been
* removed from Places. The Promise may reject if any part of constructing
* the tasks or sending them to the builder thread failed.
*/
async buildList() {
if (!(this._builder instanceof Ci.nsIJumpListBuilder)) {
console.error(
"Expected nsIJumpListBuilder. The builder is of the wrong type."
);
return;
}
// anything to build?
if (!this._showFrequent && !this._showRecent && !this._showTasks) {
// don't leave the last list hanging on the taskbar.
this._deleteActiveJumpList();
return;
}
// Are we in the midst of building an earlier iteration of this list? If
// so, bail out. Same if we're shutting down.
if (this._isBuilding || this._shuttingDown) {
return;
}
this._isBuilding = true;
try {
let removedURLs = await this._builder.checkForRemovals();
if (removedURLs.length) {
await this._clearHistory(removedURLs);
}
let selfPath = Services.dirsvc.get("XREExeF", Ci.nsIFile).path;
let taskDescriptions = [];
if (this._showTasks) {
taskDescriptions = this._tasks.map(task => {
return {
title: task.title,
description: task.description,
path: selfPath,
arguments: task.args,
fallbackIconIndex: task.iconIndex,
};
});
}
let customTitle = "";
let customDescriptions = [];
if (this._showFrequent) {
let conn = await lazy.PlacesUtils.promiseDBConnection();
let rows = await conn.executeCached(
"SELECT p.url, IFNULL(p.title, p.url) as title " +
"FROM moz_places p WHERE p.hidden = 0 " +
"AND EXISTS (" +
"SELECT id FROM moz_historyvisits WHERE " +
"place_id = p.id AND " +
"visit_type NOT IN (" +
"0, " +
`${Ci.nsINavHistoryService.TRANSITION_EMBED}, ` +
`${Ci.nsINavHistoryService.TRANSITION_FRAMED_LINK}` +
")" +
"LIMIT 1" +
") " +
"ORDER BY p.visit_count DESC LIMIT :limit",
{
limit: this._maxItemCount,
}
);
customDescriptions = rows.map(row => {
let uri = Services.io.newURI(row.getResultByName("url"));
let iconPath = "";
try {
iconPath = this._builder.obtainAndCacheFavicon(uri);
} catch (e) {
// obtainAndCacheFavicon may throw NS_ERROR_NOT_AVAILABLE if the
// icon doesn't yet exist on the disk, but has been requested.
// That's not fatal, so we'll just let it pass. Any other errors,
// however, we'll abort on.
if (e.result != Cr.NS_ERROR_NOT_AVAILABLE) {
throw e;
}
}
return {
title: row.getResultByName("title"),
description: row.getResultByName("title"),
path: selfPath,
arguments: row.getResultByName("url"),
fallbackIconIndex: 1,
iconPath,
};
});
customTitle = _getString("taskbar.frequent.label");
}
if (!this._shuttingDown) {
await this._builder.populateJumpList(
taskDescriptions,
customTitle,
customDescriptions
);
}
} catch (e) {
console.error("buildList failed: ", e);
} finally {
this._isBuilding = false;
}
}
/**
* Legacy 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 buildListLegacy() {
if (!(this._builder instanceof Ci.nsILegacyJumpListBuilder)) {
console.error(
"Expected nsILegacyJumpListBuilder. The builder is of the wrong type."
);
return;
}
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() {
if (this._builder instanceof Ci.nsIJumpListBuilder) {
this._builder.clearJumpList();
} else {
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-legacyjumplistshortcut;1"
].createInstance(Ci.nsILegacyJumpListShortcut);
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() {
aCallback.call(aScope, null);
},
});
}
/**
* Removes URLs from history in Places that the user has requested to clear
* from their Jump List. We must do this before recomputing which history
* to put into the Jump List, because if we ever include items that have
* recently been removed, Windows will not allow us to proceed.
* Please see
* for more details.
*
* The returned Promise never rejects, but may report console errors in the
* event of removal failure.
*
* @param {string[]} uriSpecsToRemove
* The URLs to be removed from Places history.
* @returns {Promise<undefined>}
*/
_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) {
return lazy.PlacesUtils.history.remove(URIsToRemove).catch(console.error);
}
return Promise.resolve();
}
};
export 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,
_useLegacyBackend: true,
/**
* Startup, shutdown, and update
*/
startup: async function WTBJL_startup() {
// We do a one-time startup read of the backend pref here because
// we don't want to consider any bugs that occur if the pref is flipped
// at runtime. We want the pref flip to only take effect on a restart.
this._useLegacyBackend = lazy._prefs.getBoolPref(
PREF_TASKBAR_LEGACY_BACKEND
);
// exit if initting the taskbar failed for some reason.
if (!(await 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;
}
if (this._shuttingDown) {
return;
}
if (this._useLegacyBackend) {
// we only need to do this once, but we do it here
// to avoid main thread io on startup
if (!this._builtPb) {
this._pbBuilder.buildListLegacy();
this._builtPb = true;
}
// do what we came here to do, update the taskbar jumplist
this._builder.buildListLegacy();
} else {
this._builder.buildList();
// We only ever need to do this once because the private browsing window
// jumplist only ever shows the static task list, which never changes,
// so it doesn't need to be updated over time.
if (!this._builtPb) {
this._pbBuilder.buildList();
this._builtPb = true;
}
}
},
_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: async function WTBJL__initTaskbar() {
let builder;
let pbBuilder;
if (this._useLegacyBackend) {
builder = lazy._taskbarService.createLegacyJumpListBuilder(false);
pbBuilder = lazy._taskbarService.createLegacyJumpListBuilder(true);
if (
!builder ||
!builder.available ||
!pbBuilder ||
!pbBuilder.available
) {
return false;
}
} else {
builder = lazy._taskbarService.createJumpListBuilder(false);
pbBuilder = lazy._taskbarService.createJumpListBuilder(true);
if (!builder || !pbBuilder) {
return false;
}
let [builderAvailable, pbBuilderAvailable] = await Promise.all([
builder.isAvailable(),
pbBuilder.isAvailable(),
]);
if (!builderAvailable || !pbBuilderAvailable) {
return false;
}
}
this._builder = new Builder(builder);
this._pbBuilder = new Builder(pbBuilder);
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();
},
QueryInterface: ChromeUtils.generateQI([
"nsINamed",
"nsIObserver",
"nsITimerCallback",
]),
name: "WinTaskbarJumpList",
notify: function WTBJL_notify() {
// 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) {
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;
}
},
};