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:
Dave Townsend 2024-11-12 15:16:46 +00:00
parent 0b8abf4e93
commit a3b8ffea41
6 changed files with 208 additions and 49 deletions

View File

@ -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
* the profile, which will notify() other running instances.

View File

@ -26,6 +26,8 @@ const NOTIFY_TIMEOUT = 200;
const COMMAND_LINE_UPDATE = "profiles-updated";
const COMMAND_LINE_ACTIVATE = "profiles-activate";
const gSupportsBadging = "nsIMacDockSupport" in Ci || "nsIWinTaskbar" in Ci;
function loadImage(url) {
return new Promise((resolve, reject) => {
let imageTools = Cc["@mozilla.org/image/tools;1"].getService(Ci.imgITools);
@ -33,6 +35,7 @@ function loadImage(url) {
let observer = imageTools.createScriptedObserver({
sizeAvailable() {
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
*/
@ -111,6 +84,7 @@ class SelectableProfileServiceClass {
#initPromise = null;
#notifyTask = null;
#observedPrefs = null;
#badge = null;
static #dirSvc = null;
// The initial preferences that will be shared amongst profiles. Only used during database
@ -410,6 +384,7 @@ class SelectableProfileServiceClass {
this.#currentProfile = null;
this.#groupToolkitProfile = null;
this.#storeID = null;
this.#badge = null;
this.#initialized = false;
}
@ -418,6 +393,17 @@ class SelectableProfileServiceClass {
lazy.EveryWindow.registerCallback(
this.#everyWindowCallbackId,
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);
if (isPBM) {
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
* notification "sps-profiles-updated" indicating that something has changed.
@ -668,6 +712,8 @@ class SelectableProfileServiceClass {
await this.loadSharedPrefsFromDatabase();
}
await this.#updateTaskbar();
if (source != "startup") {
Services.obs.notifyObservers(null, "sps-profiles-updated", source);
}
@ -1137,6 +1183,12 @@ class SelectableProfileServiceClass {
profileObj
);
if (aSelectableProfile.id == this.#currentProfile.id) {
// Force a rebuild of the taskbar icon.
this.#badge = null;
this.#currentProfile = aSelectableProfile;
}
this.#notifyTask.arm();
}
@ -1158,6 +1210,24 @@ class SelectableProfileServiceClass {
.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.
*
@ -1171,9 +1241,12 @@ class SelectableProfileServiceClass {
}
let row = (
await this.#connection.execute("SELECT * FROM Profiles WHERE id = :id;", {
id: aProfileID,
})
await this.#connection.executeCached(
"SELECT * FROM Profiles WHERE id = :id;",
{
id: aProfileID,
}
)
)[0];
return row ? new SelectableProfile(row, this) : null;

View File

@ -95,9 +95,9 @@ async function openDatabase() {
add_setup(async () => {
await SelectableProfileService.resetProfileService(gProfileService);
registerCleanupFunction(() => {
registerCleanupFunction(async () => {
SelectableProfileService.overrideDirectoryService(null);
SelectableProfileService.resetProfileService(null);
await SelectableProfileService.resetProfileService(null);
});
});

View File

@ -68,6 +68,19 @@ function getRelativeProfilePath(path) {
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() {
let dbFile = Services.dirsvc.get("UAppData", Ci.nsIFile);
dbFile.append("Profile Groups");

View File

@ -3,6 +3,60 @@ https://creativecommons.org/publicdomain/zero/1.0/ */
"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() {
startProfileService();
const SelectableProfileService = getSelectableProfileService();
@ -28,6 +82,8 @@ add_task(async function test_SelectableProfileLifecycle() {
await SelectableProfileService.maybeSetupDataStore();
let currentProfile = SelectableProfileService.currentProfile;
badgingService.assertNotBadged();
const leafName = (await currentProfile.rootDir).leafName;
const profilePath = PathUtils.join(
@ -57,6 +113,12 @@ add_task(async function test_SelectableProfileLifecycle() {
let selectableProfile = profiles[0];
Assert.equal(
selectableProfile.id,
SelectableProfileService.currentProfile.id,
"Should be the selected profile."
);
let profile = await SelectableProfileService.getProfile(selectableProfile.id);
for (let attr of ["id", "name", "path"]) {
@ -74,8 +136,13 @@ add_task(async function test_SelectableProfileLifecycle() {
}
selectableProfile.name = "updatedTestProfile";
selectableProfile.theme = {
themeId: "lightTheme",
themeFg: "#e2e1e3",
themeBg: "010203",
};
await SelectableProfileService.updateProfile(selectableProfile);
await updateNotified();
profile = await SelectableProfileService.getProfile(selectableProfile.id);
@ -85,7 +152,12 @@ add_task(async function test_SelectableProfileLifecycle() {
"We got the correct profile name: updatedTestProfile"
);
badgingService.assertNotBadged();
let newProfile = await createTestProfile({ name: "New profile" });
await updateNotified();
let rootDir = await newProfile.rootDir;
let localDir = PathUtils.join(
Services.dirsvc.get("DefProfLRt", Ci.nsIFile).path,
@ -135,11 +207,16 @@ add_task(async function test_SelectableProfileLifecycle() {
profiles = await SelectableProfileService.getAllProfiles();
Assert.equal(profiles.length, 2, "Should now be two profiles.");
badgingService.assertBadged("#e2e1e3", "010203");
await SelectableProfileService.deleteProfile(newProfile);
await updateNotified();
profiles = await SelectableProfileService.getAllProfiles();
Assert.equal(profiles.length, 1, "Should now be one profiles.");
badgingService.assertNotBadged();
profileDirExists = await IOUtils.exists(rootDir.path);
profileLocalDirExists = await IOUtils.exists(localDir);
Assert.ok(!profileDirExists, "Profile dir was successfully removed");

View File

@ -24,19 +24,6 @@ add_setup(async () => {
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() {
const SelectableProfileService = getSelectableProfileService();
let prefs = await SelectableProfileService.getAllDBPrefs();