Bug 1926507: Badge the taskbar/dock with the profile avatar. r=niklas,jhirsch
Differential Revision: https://phabricator.services.mozilla.com/D228439
This commit is contained in:
@@ -134,6 +134,15 @@ export class SelectableProfile {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get iconPaintContext() {
|
||||||
|
return {
|
||||||
|
fillColor: this.#themeBg,
|
||||||
|
strokeColor: this.#themeFg,
|
||||||
|
fillOpacity: 1.0,
|
||||||
|
strokeOpacity: 1.0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the theme (all three properties are required), then trigger saving
|
* Update the theme (all three properties are required), then trigger saving
|
||||||
* the profile, which will notify() other running instances.
|
* the profile, which will notify() other running instances.
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ const NOTIFY_TIMEOUT = 200;
|
|||||||
const COMMAND_LINE_UPDATE = "profiles-updated";
|
const COMMAND_LINE_UPDATE = "profiles-updated";
|
||||||
const COMMAND_LINE_ACTIVATE = "profiles-activate";
|
const COMMAND_LINE_ACTIVATE = "profiles-activate";
|
||||||
|
|
||||||
|
const gSupportsBadging = "nsIMacDockSupport" in Ci || "nsIWinTaskbar" in Ci;
|
||||||
|
|
||||||
function loadImage(url) {
|
function loadImage(url) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let imageTools = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools);
|
let imageTools = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools);
|
||||||
@@ -33,6 +35,7 @@ function loadImage(url) {
|
|||||||
let observer = imageTools.createScriptedObserver({
|
let observer = imageTools.createScriptedObserver({
|
||||||
sizeAvailable() {
|
sizeAvailable() {
|
||||||
resolve(imageContainer);
|
resolve(imageContainer);
|
||||||
|
imageContainer = null;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -58,36 +61,6 @@ function loadImage(url) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is waiting to be used by bug 1926507.
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
async function updateTaskbar(iconUrl, profileName, strokeColor, fillColor) {
|
|
||||||
try {
|
|
||||||
let image = await loadImage(iconUrl);
|
|
||||||
|
|
||||||
if ("nsIMacDockSupport" in Ci) {
|
|
||||||
Cc["@mozilla.org/widget/macdocksupport;1"]
|
|
||||||
.getService(Ci.nsIMacDockSupport)
|
|
||||||
.setBadgeImage(image, { fillColor, strokeColor });
|
|
||||||
} else if ("nsIWinTaskbar" in Ci) {
|
|
||||||
lazy.EveryWindow.registerCallback(
|
|
||||||
"profiles",
|
|
||||||
win => {
|
|
||||||
let iconController = Cc["@mozilla.org/windows-taskbar;1"]
|
|
||||||
.getService(Ci.nsIWinTaskbar)
|
|
||||||
.getOverlayIconController(win.docShell);
|
|
||||||
iconController.setOverlayIcon(image, profileName, {
|
|
||||||
fillColor,
|
|
||||||
strokeColor,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
() => {}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The service that manages selectable profiles
|
* The service that manages selectable profiles
|
||||||
*/
|
*/
|
||||||
@@ -111,6 +84,7 @@ class SelectableProfileServiceClass {
|
|||||||
#initPromise = null;
|
#initPromise = null;
|
||||||
#notifyTask = null;
|
#notifyTask = null;
|
||||||
#observedPrefs = null;
|
#observedPrefs = null;
|
||||||
|
#badge = null;
|
||||||
static #dirSvc = null;
|
static #dirSvc = null;
|
||||||
|
|
||||||
// The initial preferences that will be shared amongst profiles. Only used during database
|
// The initial preferences that will be shared amongst profiles. Only used during database
|
||||||
@@ -410,6 +384,7 @@ class SelectableProfileServiceClass {
|
|||||||
this.#currentProfile = null;
|
this.#currentProfile = null;
|
||||||
this.#groupToolkitProfile = null;
|
this.#groupToolkitProfile = null;
|
||||||
this.#storeID = null;
|
this.#storeID = null;
|
||||||
|
this.#badge = null;
|
||||||
|
|
||||||
this.#initialized = false;
|
this.#initialized = false;
|
||||||
}
|
}
|
||||||
@@ -418,6 +393,17 @@ class SelectableProfileServiceClass {
|
|||||||
lazy.EveryWindow.registerCallback(
|
lazy.EveryWindow.registerCallback(
|
||||||
this.#everyWindowCallbackId,
|
this.#everyWindowCallbackId,
|
||||||
window => {
|
window => {
|
||||||
|
if (this.#badge && "nsIWinTaskbar" in Ci) {
|
||||||
|
let iconController = Cc["@mozilla.org/windows-taskbar;1"]
|
||||||
|
.getService(Ci.nsIWinTaskbar)
|
||||||
|
.getOverlayIconController(window.docShell);
|
||||||
|
iconController.setOverlayIcon(
|
||||||
|
this.#badge.image,
|
||||||
|
this.#badge.description,
|
||||||
|
this.#badge.iconPaintContext
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
let isPBM = lazy.PrivateBrowsingUtils.isWindowPrivate(window);
|
let isPBM = lazy.PrivateBrowsingUtils.isWindowPrivate(window);
|
||||||
if (isPBM) {
|
if (isPBM) {
|
||||||
return;
|
return;
|
||||||
@@ -653,6 +639,64 @@ class SelectableProfileServiceClass {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async #updateTaskbar() {
|
||||||
|
try {
|
||||||
|
if (!gSupportsBadging) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let count = await this.getProfileCount();
|
||||||
|
|
||||||
|
if (count > 1 && !this.#badge) {
|
||||||
|
this.#badge = {
|
||||||
|
image: await loadImage(
|
||||||
|
Services.io.newURI(
|
||||||
|
`chrome://browser/content/profiles/assets/48_${
|
||||||
|
this.#currentProfile.avatar
|
||||||
|
}.svg`
|
||||||
|
)
|
||||||
|
),
|
||||||
|
iconPaintContext: this.#currentProfile.iconPaintContext,
|
||||||
|
description: this.#currentProfile.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
if ("nsIMacDockSupport" in Ci) {
|
||||||
|
Cc["@mozilla.org/widget/macdocksupport;1"]
|
||||||
|
.getService(Ci.nsIMacDockSupport)
|
||||||
|
.setBadgeImage(this.#badge.image, this.#badge.iconPaintContext);
|
||||||
|
} else if ("nsIWinTaskbar" in Ci) {
|
||||||
|
for (let win of lazy.EveryWindow.readyWindows) {
|
||||||
|
let iconController = Cc["@mozilla.org/windows-taskbar;1"]
|
||||||
|
.getService(Ci.nsIWinTaskbar)
|
||||||
|
.getOverlayIconController(win.docShell);
|
||||||
|
iconController.setOverlayIcon(
|
||||||
|
this.#badge.image,
|
||||||
|
this.#badge.description,
|
||||||
|
this.#badge.iconPaintContext
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (count <= 1 && this.#badge) {
|
||||||
|
this.#badge = null;
|
||||||
|
|
||||||
|
if ("nsIMacDockSupport" in Ci) {
|
||||||
|
Cc["@mozilla.org/widget/macdocksupport;1"]
|
||||||
|
.getService(Ci.nsIMacDockSupport)
|
||||||
|
.setBadgeImage(null);
|
||||||
|
} else if ("nsIWinTaskbar" in Ci) {
|
||||||
|
for (let win of lazy.EveryWindow.readyWindows) {
|
||||||
|
let iconController = Cc["@mozilla.org/windows-taskbar;1"]
|
||||||
|
.getService(Ci.nsIWinTaskbar)
|
||||||
|
.getOverlayIconController(win.docShell);
|
||||||
|
iconController.setOverlayIcon(null, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Invoked when changes have been made to the database. Sends the observer
|
* Invoked when changes have been made to the database. Sends the observer
|
||||||
* notification "sps-profiles-updated" indicating that something has changed.
|
* notification "sps-profiles-updated" indicating that something has changed.
|
||||||
@@ -668,6 +712,8 @@ class SelectableProfileServiceClass {
|
|||||||
await this.loadSharedPrefsFromDatabase();
|
await this.loadSharedPrefsFromDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await this.#updateTaskbar();
|
||||||
|
|
||||||
if (source != "startup") {
|
if (source != "startup") {
|
||||||
Services.obs.notifyObservers(null, "sps-profiles-updated", source);
|
Services.obs.notifyObservers(null, "sps-profiles-updated", source);
|
||||||
}
|
}
|
||||||
@@ -1137,6 +1183,12 @@ class SelectableProfileServiceClass {
|
|||||||
profileObj
|
profileObj
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (aSelectableProfile.id == this.#currentProfile.id) {
|
||||||
|
// Force a rebuild of the taskbar icon.
|
||||||
|
this.#badge = null;
|
||||||
|
this.#currentProfile = aSelectableProfile;
|
||||||
|
}
|
||||||
|
|
||||||
this.#notifyTask.arm();
|
this.#notifyTask.arm();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1158,6 +1210,24 @@ class SelectableProfileServiceClass {
|
|||||||
.sort((p1, p2) => p1.name.localeCompare(p2.name));
|
.sort((p1, p2) => p1.name.localeCompare(p2.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the number of profiles in the group.
|
||||||
|
*
|
||||||
|
* @returns {number}
|
||||||
|
* The number of profiles in the group.
|
||||||
|
*/
|
||||||
|
async getProfileCount() {
|
||||||
|
if (!this.#connection) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rows = await this.#connection.executeCached(
|
||||||
|
'SELECT COUNT(*) AS "count" FROM "Profiles";'
|
||||||
|
);
|
||||||
|
|
||||||
|
return rows[0]?.getResultByName("count") ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a specific profile by its internal ID.
|
* Get a specific profile by its internal ID.
|
||||||
*
|
*
|
||||||
@@ -1171,9 +1241,12 @@ class SelectableProfileServiceClass {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let row = (
|
let row = (
|
||||||
await this.#connection.execute("SELECT * FROM Profiles WHERE id = :id;", {
|
await this.#connection.executeCached(
|
||||||
id: aProfileID,
|
"SELECT * FROM Profiles WHERE id = :id;",
|
||||||
})
|
{
|
||||||
|
id: aProfileID,
|
||||||
|
}
|
||||||
|
)
|
||||||
)[0];
|
)[0];
|
||||||
|
|
||||||
return row ? new SelectableProfile(row, this) : null;
|
return row ? new SelectableProfile(row, this) : null;
|
||||||
|
|||||||
@@ -95,9 +95,9 @@ async function openDatabase() {
|
|||||||
add_setup(async () => {
|
add_setup(async () => {
|
||||||
await SelectableProfileService.resetProfileService(gProfileService);
|
await SelectableProfileService.resetProfileService(gProfileService);
|
||||||
|
|
||||||
registerCleanupFunction(() => {
|
registerCleanupFunction(async () => {
|
||||||
SelectableProfileService.overrideDirectoryService(null);
|
SelectableProfileService.overrideDirectoryService(null);
|
||||||
SelectableProfileService.resetProfileService(null);
|
await SelectableProfileService.resetProfileService(null);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,19 @@ function getRelativeProfilePath(path) {
|
|||||||
return relativePath;
|
return relativePath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Waits for the profile service to update about a change
|
||||||
|
async function updateNotified() {
|
||||||
|
let { resolve, promise } = Promise.withResolvers();
|
||||||
|
let observer = (subject, topic, data) => {
|
||||||
|
Services.obs.removeObserver(observer, "sps-profiles-updated");
|
||||||
|
resolve(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
Services.obs.addObserver(observer, "sps-profiles-updated");
|
||||||
|
|
||||||
|
await promise;
|
||||||
|
}
|
||||||
|
|
||||||
async function openDatabase() {
|
async function openDatabase() {
|
||||||
let dbFile = Services.dirsvc.get("UAppData", Ci.nsIFile);
|
let dbFile = Services.dirsvc.get("UAppData", Ci.nsIFile);
|
||||||
dbFile.append("Profile Groups");
|
dbFile.append("Profile Groups");
|
||||||
|
|||||||
@@ -3,6 +3,60 @@ https://creativecommons.org/publicdomain/zero/1.0/ */
|
|||||||
|
|
||||||
"use strict";
|
"use strict";
|
||||||
|
|
||||||
|
const { MockRegistrar } = ChromeUtils.importESModule(
|
||||||
|
"resource://testing-common/MockRegistrar.sys.mjs"
|
||||||
|
);
|
||||||
|
|
||||||
|
const badgingService = {
|
||||||
|
isRegistered: false,
|
||||||
|
badge: null,
|
||||||
|
|
||||||
|
// nsIMacDockSupport
|
||||||
|
setBadgeImage(image, paintContext) {
|
||||||
|
this.badge = { image, paintContext };
|
||||||
|
},
|
||||||
|
|
||||||
|
QueryInterface: ChromeUtils.generateQI(["nsIMacDockSupport"]),
|
||||||
|
|
||||||
|
assertBadged(fg, bg) {
|
||||||
|
if (!this.isRegistered) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.ok(this.badge?.image, "Should have set a badge image");
|
||||||
|
Assert.ok(this.badge?.paintContext, "Should have set a paint context");
|
||||||
|
Assert.equal(
|
||||||
|
this.badge?.paintContext?.strokeColor,
|
||||||
|
fg,
|
||||||
|
"Stroke color should be correct"
|
||||||
|
);
|
||||||
|
Assert.equal(
|
||||||
|
this.badge?.paintContext?.fillColor,
|
||||||
|
bg,
|
||||||
|
"Stroke color should be correct"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
assertNotBadged() {
|
||||||
|
if (!this.isRegistered) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Assert.ok(!this.badge?.image, "Should not have set a badge image");
|
||||||
|
Assert.ok(!this.badge?.paintContext, "Should not have set a paint context");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
add_setup(() => {
|
||||||
|
if ("nsIMacDockSupport" in Ci) {
|
||||||
|
badgingService.isRegistered = true;
|
||||||
|
MockRegistrar.register(
|
||||||
|
"@mozilla.org/widget/macdocksupport;1",
|
||||||
|
badgingService
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
add_task(async function test_SelectableProfileLifecycle() {
|
add_task(async function test_SelectableProfileLifecycle() {
|
||||||
startProfileService();
|
startProfileService();
|
||||||
const SelectableProfileService = getSelectableProfileService();
|
const SelectableProfileService = getSelectableProfileService();
|
||||||
@@ -28,6 +82,8 @@ add_task(async function test_SelectableProfileLifecycle() {
|
|||||||
await SelectableProfileService.maybeSetupDataStore();
|
await SelectableProfileService.maybeSetupDataStore();
|
||||||
let currentProfile = SelectableProfileService.currentProfile;
|
let currentProfile = SelectableProfileService.currentProfile;
|
||||||
|
|
||||||
|
badgingService.assertNotBadged();
|
||||||
|
|
||||||
const leafName = (await currentProfile.rootDir).leafName;
|
const leafName = (await currentProfile.rootDir).leafName;
|
||||||
|
|
||||||
const profilePath = PathUtils.join(
|
const profilePath = PathUtils.join(
|
||||||
@@ -57,6 +113,12 @@ add_task(async function test_SelectableProfileLifecycle() {
|
|||||||
|
|
||||||
let selectableProfile = profiles[0];
|
let selectableProfile = profiles[0];
|
||||||
|
|
||||||
|
Assert.equal(
|
||||||
|
selectableProfile.id,
|
||||||
|
SelectableProfileService.currentProfile.id,
|
||||||
|
"Should be the selected profile."
|
||||||
|
);
|
||||||
|
|
||||||
let profile = await SelectableProfileService.getProfile(selectableProfile.id);
|
let profile = await SelectableProfileService.getProfile(selectableProfile.id);
|
||||||
|
|
||||||
for (let attr of ["id", "name", "path"]) {
|
for (let attr of ["id", "name", "path"]) {
|
||||||
@@ -74,8 +136,13 @@ add_task(async function test_SelectableProfileLifecycle() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
selectableProfile.name = "updatedTestProfile";
|
selectableProfile.name = "updatedTestProfile";
|
||||||
|
selectableProfile.theme = {
|
||||||
|
themeId: "lightTheme",
|
||||||
|
themeFg: "#e2e1e3",
|
||||||
|
themeBg: "010203",
|
||||||
|
};
|
||||||
|
|
||||||
await SelectableProfileService.updateProfile(selectableProfile);
|
await updateNotified();
|
||||||
|
|
||||||
profile = await SelectableProfileService.getProfile(selectableProfile.id);
|
profile = await SelectableProfileService.getProfile(selectableProfile.id);
|
||||||
|
|
||||||
@@ -85,7 +152,12 @@ add_task(async function test_SelectableProfileLifecycle() {
|
|||||||
"We got the correct profile name: updatedTestProfile"
|
"We got the correct profile name: updatedTestProfile"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
badgingService.assertNotBadged();
|
||||||
|
|
||||||
let newProfile = await createTestProfile({ name: "New profile" });
|
let newProfile = await createTestProfile({ name: "New profile" });
|
||||||
|
|
||||||
|
await updateNotified();
|
||||||
|
|
||||||
let rootDir = await newProfile.rootDir;
|
let rootDir = await newProfile.rootDir;
|
||||||
let localDir = PathUtils.join(
|
let localDir = PathUtils.join(
|
||||||
Services.dirsvc.get("DefProfLRt", Ci.nsIFile).path,
|
Services.dirsvc.get("DefProfLRt", Ci.nsIFile).path,
|
||||||
@@ -138,11 +210,16 @@ add_task(async function test_SelectableProfileLifecycle() {
|
|||||||
profiles = await SelectableProfileService.getAllProfiles();
|
profiles = await SelectableProfileService.getAllProfiles();
|
||||||
Assert.equal(profiles.length, 2, "Should now be two profiles.");
|
Assert.equal(profiles.length, 2, "Should now be two profiles.");
|
||||||
|
|
||||||
|
badgingService.assertBadged("#e2e1e3", "010203");
|
||||||
|
|
||||||
await SelectableProfileService.deleteProfile(newProfile);
|
await SelectableProfileService.deleteProfile(newProfile);
|
||||||
|
await updateNotified();
|
||||||
|
|
||||||
profiles = await SelectableProfileService.getAllProfiles();
|
profiles = await SelectableProfileService.getAllProfiles();
|
||||||
Assert.equal(profiles.length, 1, "Should now be one profiles.");
|
Assert.equal(profiles.length, 1, "Should now be one profiles.");
|
||||||
|
|
||||||
|
badgingService.assertNotBadged();
|
||||||
|
|
||||||
profileDirExists = await IOUtils.exists(rootDir.path);
|
profileDirExists = await IOUtils.exists(rootDir.path);
|
||||||
profileLocalDirExists = await IOUtils.exists(localDir);
|
profileLocalDirExists = await IOUtils.exists(localDir);
|
||||||
Assert.ok(!profileDirExists, "Profile dir was successfully removed");
|
Assert.ok(!profileDirExists, "Profile dir was successfully removed");
|
||||||
|
|||||||
@@ -24,19 +24,6 @@ add_setup(async () => {
|
|||||||
await SelectableProfileService.maybeSetupDataStore();
|
await SelectableProfileService.maybeSetupDataStore();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Waits for the profile service to update about a change
|
|
||||||
async function updateNotified() {
|
|
||||||
let { resolve, promise } = Promise.withResolvers();
|
|
||||||
let observer = (subject, topic, data) => {
|
|
||||||
Services.obs.removeObserver(observer, "sps-profiles-updated");
|
|
||||||
resolve(data);
|
|
||||||
};
|
|
||||||
|
|
||||||
Services.obs.addObserver(observer, "sps-profiles-updated");
|
|
||||||
|
|
||||||
await promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
add_task(async function test_SharedPrefsLifecycle() {
|
add_task(async function test_SharedPrefsLifecycle() {
|
||||||
const SelectableProfileService = getSelectableProfileService();
|
const SelectableProfileService = getSelectableProfileService();
|
||||||
let prefs = await SelectableProfileService.getAllDBPrefs();
|
let prefs = await SelectableProfileService.getAllDBPrefs();
|
||||||
|
|||||||
Reference in New Issue
Block a user