diff --git a/locales/en-US/browser/browser/preferences/zen-preferences.ftl b/locales/en-US/browser/browser/preferences/zen-preferences.ftl
index c803f73046..a43505f87b 100644
--- a/locales/en-US/browser/browser/preferences/zen-preferences.ftl
+++ b/locales/en-US/browser/browser/preferences/zen-preferences.ftl
@@ -48,6 +48,14 @@ zen-look-and-feel-compact-view-top-toolbar =
zen-look-and-feel-compact-toolbar-flash-popup =
.label = Briefly make the toolbar popup when switching or opening new tabs in compact mode
+zen-look-and-feel-app-icon-header = App Icon
+zen-look-and-feel-app-icon-description = Change the icon used by the running app on macOS
+zen-look-and-feel-app-icon-label = Variant
+zen-look-and-feel-app-icon-default =
+ .label = Default
+zen-look-and-feel-app-icon-alternate =
+ .label = Alternative
+
pane-zen-tabs-title = Tab Management
category-zen-workspaces =
.tooltiptext = { pane-zen-tabs-title }
@@ -357,4 +365,4 @@ zen-devtools-toggle-dom-shortcut = Toggle DOM
zen-devtools-toggle-accessibility-shortcut = Toggle Accessibility
zen-close-all-unpinned-tabs-shortcut = Close All Unpinned Tabs
zen-new-unsynced-window-shortcut = New Blank Window
-zen-duplicate-tab-shortcut = Duplicate Tab
\ No newline at end of file
+zen-duplicate-tab-shortcut = Duplicate Tab
diff --git a/src/browser/components/preferences/zen-settings.js b/src/browser/components/preferences/zen-settings.js
index 16ddcbc63e..684da50ec9 100644
--- a/src/browser/components/preferences/zen-settings.js
+++ b/src/browser/components/preferences/zen-settings.js
@@ -644,6 +644,16 @@ var gZenMarketplaceManager = {
const kZenExtendedSidebar = "zen.view.sidebar-expanded";
const kZenSingleToolbar = "zen.view.use-single-toolbar";
+const kZenMacOSAppIconVariant = "zen.widget.macos.app-icon-variant";
+const kZenMacOSAppIconPreviewSrcByVariant = {
+ default: "chrome://branding/content/icon48.png",
+ alternate: "chrome://browser/content/zen-images/app-icons/alternate-preview.png",
+};
+
+function getZenMacOSAppIconVariant() {
+ const variant = Services.prefs.getStringPref(kZenMacOSAppIconVariant, "default");
+ return kZenMacOSAppIconPreviewSrcByVariant[variant] ? variant : "default";
+}
var gZenLooksAndFeel = {
init() {
@@ -652,18 +662,25 @@ var gZenLooksAndFeel = {
}
this.__hasInitialized = true;
gZenMarketplaceManager.init();
- for (const pref of [kZenExtendedSidebar, kZenSingleToolbar]) {
+ for (const pref of [kZenExtendedSidebar, kZenSingleToolbar, kZenMacOSAppIconVariant]) {
Services.prefs.addObserver(pref, this);
}
window.addEventListener("unload", () => {
- for (const pref of [kZenExtendedSidebar, kZenSingleToolbar]) {
+ for (const pref of [kZenExtendedSidebar, kZenSingleToolbar, kZenMacOSAppIconVariant]) {
Services.prefs.removeObserver(pref, this);
}
});
this.applySidebarLayout();
+ this.initMacOSAppIconVariantControl();
+ this.applyMacOSAppIconPreview();
},
- observe() {
+ observe(_subject, _topic, data) {
+ if (data == kZenMacOSAppIconVariant) {
+ this.applyMacOSAppIconPreview();
+ return;
+ }
+
this.applySidebarLayout();
},
@@ -708,6 +725,27 @@ var gZenLooksAndFeel = {
});
}
},
+
+ applyMacOSAppIconPreview() {
+ const preview = document.getElementById("zenLooksAndFeelMacOSAppIconPreview");
+ if (!preview) {
+ return;
+ }
+
+ preview.setAttribute(
+ "src",
+ kZenMacOSAppIconPreviewSrcByVariant[getZenMacOSAppIconVariant()]
+ );
+ },
+
+ initMacOSAppIconVariantControl() {
+ const variantControl = document.getElementById("zenLooksAndFeelMacOSAppIconVariant");
+ if (!variantControl) {
+ return;
+ }
+
+ Preferences.addSyncFromPrefListener(variantControl, getZenMacOSAppIconVariant);
+ },
};
var gZenWorkspacesSettings = {
@@ -1135,6 +1173,11 @@ Preferences.addAll([
type: "bool",
default: true,
},
+ {
+ id: "zen.widget.macos.app-icon-variant",
+ type: "string",
+ default: "default",
+ },
{
id: "zen.workspaces.hide-default-container-indicator",
type: "bool",
diff --git a/src/browser/components/preferences/zenLooksAndFeel.inc.xhtml b/src/browser/components/preferences/zenLooksAndFeel.inc.xhtml
index 08b2c2ef7f..9b696bbd35 100644
--- a/src/browser/components/preferences/zenLooksAndFeel.inc.xhtml
+++ b/src/browser/components/preferences/zenLooksAndFeel.inc.xhtml
@@ -61,6 +61,33 @@
data-l10n-id="zen-look-and-feel-compact-toolbar-flash-popup"
preference="zen.view.compact.toolbar-flash-popup"/>
+
+#ifdef XP_MACOSX
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+#endif
+
+
+
+ CFBundleIdentifier
+ app.zen-browser.icons.alternate
+ CFBundlePackageType
+ BNDL
+
+
diff --git a/src/zen/images/app-icons/alternate.bundle/Contents/Resources/Assets.car b/src/zen/images/app-icons/alternate.bundle/Contents/Resources/Assets.car
new file mode 100644
index 0000000000..2cdedc8a46
Binary files /dev/null and b/src/zen/images/app-icons/alternate.bundle/Contents/Resources/Assets.car differ
diff --git a/src/zen/images/jar.inc.mn b/src/zen/images/jar.inc.mn
index d5ac5ccd76..9ead4c2124 100644
--- a/src/zen/images/jar.inc.mn
+++ b/src/zen/images/jar.inc.mn
@@ -5,6 +5,7 @@
content/browser/zen-images/layouts/collapsed.png (../../zen/images/layouts/collapsed.png)
content/browser/zen-images/layouts/multiple-toolbar.png (../../zen/images/layouts/multiple-toolbar.png)
content/browser/zen-images/layouts/single-toolbar.png (../../zen/images/layouts/single-toolbar.png)
+ content/browser/zen-images/app-icons/alternate-preview.png (../../zen/images/app-icons/alternate-preview.png)
content/browser/zen-images/grain-bg.png (../../zen/images/grain-bg.png)
content/browser/zen-images/note-indicator.svg (../../zen/images/note-indicator.svg)
diff --git a/src/zen/toolkit/common/ZenCommonUtils.cpp b/src/zen/toolkit/common/ZenCommonUtils.cpp
index a8d1e478d1..5a6ccbde09 100644
--- a/src/zen/toolkit/common/ZenCommonUtils.cpp
+++ b/src/zen/toolkit/common/ZenCommonUtils.cpp
@@ -69,6 +69,12 @@ ZenCommonUtils::PlayHapticFeedback() {
return PlayHapticFeedbackInternal();
}
+NS_IMETHODIMP
+ZenCommonUtils::SetMacOSAppIcon(const nsAString& aIconBundlePath,
+ const nsAString& aIconName) {
+ return SetMacOSAppIconInternal(aIconBundlePath, aIconName);
+}
+
NS_IMETHODIMP
ZenCommonUtils::CanShare(bool* canShare) {
auto aWindow = GetMostRecentWindow();
diff --git a/src/zen/toolkit/common/ZenCommonUtils.h b/src/zen/toolkit/common/ZenCommonUtils.h
index 48cc03a6e8..56ccb452a7 100644
--- a/src/zen/toolkit/common/ZenCommonUtils.h
+++ b/src/zen/toolkit/common/ZenCommonUtils.h
@@ -51,8 +51,15 @@ class ZenCommonUtils final : public nsIZenCommonUtils {
// No-op on non-macOS platforms
return NS_OK;
}
+ static auto SetMacOSAppIconInternal(const nsAString& aIconBundlePath,
+ const nsAString& aIconName) -> nsresult {
+ // No-op on non-macOS platforms
+ return NS_OK;
+ }
#else
static auto PlayHapticFeedbackInternal() -> nsresult;
+ static auto SetMacOSAppIconInternal(const nsAString& aIconBundlePath,
+ const nsAString& aIconName) -> nsresult;
#endif
};
diff --git a/src/zen/toolkit/common/cocoa/ZenAppIcon.mm b/src/zen/toolkit/common/cocoa/ZenAppIcon.mm
new file mode 100644
index 0000000000..4d16e1cac5
--- /dev/null
+++ b/src/zen/toolkit/common/cocoa/ZenAppIcon.mm
@@ -0,0 +1,212 @@
+/* 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/. */
+
+#include "ZenCommonUtils.h"
+#include "nsCocoaUtils.h"
+
+#include "nsString.h"
+
+#import
+
+namespace zen {
+
+namespace {
+
+constexpr CGFloat kMacOSAppIconSize = 1024.0;
+NSUInteger sRunningIconGeneration = 0;
+
+struct MacOSAppIconImages {
+ NSImage* bundleIconImage = nil;
+ NSImage* runningIconImage = nil;
+};
+
+auto ApplyRunningMacOSAppIconImage(NSImage* aIconImage) -> void {
+ [NSApp setApplicationIconImage:aIconImage];
+ [[NSApp dockTile] display];
+}
+
+auto SetRunningMacOSAppIconImage(NSImage* aIconImage) -> void {
+ NSUInteger generation = ++sRunningIconGeneration;
+ ApplyRunningMacOSAppIconImage(aIconImage);
+
+ NSImage* iconImage = [aIconImage retain];
+ dispatch_after(
+ dispatch_time(DISPATCH_TIME_NOW, static_cast(0.5 * NSEC_PER_SEC)),
+ dispatch_get_main_queue(), ^{
+ if (generation == sRunningIconGeneration) {
+ ApplyRunningMacOSAppIconImage(iconImage);
+ }
+ [iconImage release];
+ });
+}
+
+auto SetMacOSAppBundleIconOverride(NSImage* aIconImage) -> void {
+ NSString* bundlePath = [[NSBundle mainBundle] bundlePath];
+ if (bundlePath) {
+ [[NSWorkspace sharedWorkspace] setIcon:aIconImage
+ forFile:bundlePath
+ options:0];
+ }
+}
+
+auto CopyRasterizedMacOSAppIconImage(NSImage* aIconImage) -> NSImage* {
+ if (!aIconImage) {
+ return nil;
+ }
+
+ NSSize iconSize = NSMakeSize(kMacOSAppIconSize, kMacOSAppIconSize);
+ NSBitmapImageRep* bitmap = [[NSBitmapImageRep alloc]
+ initWithBitmapDataPlanes:nullptr
+ pixelsWide:static_cast(iconSize.width)
+ pixelsHigh:static_cast(iconSize.height)
+ bitsPerSample:8
+ samplesPerPixel:4
+ hasAlpha:YES
+ isPlanar:NO
+ colorSpaceName:NSDeviceRGBColorSpace
+ bytesPerRow:0
+ bitsPerPixel:0];
+ if (!bitmap) {
+ return nil;
+ }
+
+ [bitmap setSize:iconSize];
+ NSGraphicsContext* context =
+ [NSGraphicsContext graphicsContextWithBitmapImageRep:bitmap];
+ if (!context) {
+ [bitmap release];
+ return nil;
+ }
+
+ [NSGraphicsContext saveGraphicsState];
+ [NSGraphicsContext setCurrentContext:context];
+ [context setImageInterpolation:NSImageInterpolationHigh];
+ [aIconImage drawInRect:NSMakeRect(0, 0, iconSize.width, iconSize.height)
+ fromRect:NSZeroRect
+ operation:NSCompositingOperationSourceOver
+ fraction:1.0
+ respectFlipped:NO
+ hints:nil];
+ [NSGraphicsContext restoreGraphicsState];
+
+ NSImage* rasterizedImage = [[[NSImage alloc] initWithSize:iconSize] autorelease];
+ [rasterizedImage addRepresentation:bitmap];
+ [bitmap release];
+
+ return rasterizedImage;
+}
+
+auto SetMacOSAppIconImage(const MacOSAppIconImages& aIconImages) -> nsresult {
+ NSImage* bundleIconImage =
+ CopyRasterizedMacOSAppIconImage(aIconImages.bundleIconImage);
+ NSImage* runningIconImage =
+ CopyRasterizedMacOSAppIconImage(aIconImages.runningIconImage);
+ if (!bundleIconImage || !runningIconImage) {
+ return NS_ERROR_FAILURE;
+ }
+
+ SetMacOSAppBundleIconOverride(bundleIconImage);
+ SetRunningMacOSAppIconImage(runningIconImage);
+ return NS_OK;
+}
+
+auto LoadDefaultMacOSAppIconImage() -> NSImage* {
+ NSBundle* mainBundle = [NSBundle mainBundle];
+ NSString* resourcePath = [mainBundle resourcePath];
+ NSString* iconFile =
+ [mainBundle objectForInfoDictionaryKey:@"CFBundleIconFile"];
+ if (!resourcePath || !iconFile) {
+ return nil;
+ }
+
+ if ([[iconFile pathExtension] length] == 0) {
+ iconFile = [iconFile stringByAppendingPathExtension:@"icns"];
+ }
+
+ NSString* iconPath = [resourcePath stringByAppendingPathComponent:iconFile];
+ return [[[NSImage alloc] initWithContentsOfFile:iconPath] autorelease];
+}
+
+auto ResetMacOSAppIconImage() -> nsresult {
+ NSImage* defaultIconImage = LoadDefaultMacOSAppIconImage();
+ if (!defaultIconImage) {
+ return NS_ERROR_FAILURE;
+ }
+
+ SetMacOSAppBundleIconOverride(nil);
+ SetRunningMacOSAppIconImage(defaultIconImage);
+ return NS_OK;
+}
+
+auto LoadMacOSRunningAppIconImage(NSBundle* aIconBundle, NSString* aIconName)
+ -> NSImage* {
+ NSArray* imageNames = @[
+ [aIconName stringByAppendingString:@"Preview"],
+ @"alternate-preview",
+ aIconName,
+ ];
+
+ for (NSString* imageName in imageNames) {
+ NSImage* iconImage = [aIconBundle imageForResource:imageName];
+ if (iconImage) {
+ return iconImage;
+ }
+ }
+
+ return nil;
+}
+
+auto LoadMacOSAppIconImages(const nsAString& aIconBundlePath,
+ const nsAString& aIconName) -> MacOSAppIconImages {
+ NSString* resourcePath = [[NSBundle mainBundle] resourcePath];
+ if (!resourcePath) {
+ return {};
+ }
+
+ NSString* iconBundlePath = nsCocoaUtils::ToNSString(aIconBundlePath);
+ NSString* iconName = nsCocoaUtils::ToNSString(aIconName);
+ NSArray* bundlePaths = @[
+ [resourcePath stringByAppendingPathComponent:iconBundlePath],
+ [[resourcePath stringByAppendingPathComponent:@"browser"]
+ stringByAppendingPathComponent:iconBundlePath],
+ ];
+
+ for (NSString* bundlePath in bundlePaths) {
+ NSBundle* iconBundle = [NSBundle bundleWithPath:bundlePath];
+ NSImage* bundleIconImage = [iconBundle imageForResource:iconName];
+ if (bundleIconImage) {
+ return {bundleIconImage,
+ LoadMacOSRunningAppIconImage(iconBundle, iconName)};
+ }
+ }
+
+ return {};
+}
+
+} // namespace
+
+auto ZenCommonUtils::SetMacOSAppIconInternal(
+ const nsAString& aIconBundlePath, const nsAString& aIconName) -> nsresult {
+ if (!NSApp) {
+ return NS_ERROR_NOT_AVAILABLE;
+ }
+
+ if (aIconBundlePath.IsEmpty() && aIconName.IsEmpty()) {
+ return ResetMacOSAppIconImage();
+ }
+
+ if (aIconBundlePath.IsEmpty() || aIconName.IsEmpty()) {
+ return NS_ERROR_INVALID_ARG;
+ }
+
+ MacOSAppIconImages iconImages =
+ LoadMacOSAppIconImages(aIconBundlePath, aIconName);
+ if (!iconImages.bundleIconImage || !iconImages.runningIconImage) {
+ return NS_ERROR_FAILURE;
+ }
+
+ return SetMacOSAppIconImage(iconImages);
+}
+
+} // namespace zen
diff --git a/src/zen/toolkit/common/cocoa/moz.build b/src/zen/toolkit/common/cocoa/moz.build
index afd519605a..ae9ee1e0d8 100644
--- a/src/zen/toolkit/common/cocoa/moz.build
+++ b/src/zen/toolkit/common/cocoa/moz.build
@@ -4,6 +4,7 @@
FINAL_LIBRARY = "xul"
SOURCES += [
+ "ZenAppIcon.mm",
"ZenHapticFeedback.mm",
"ZenShareInternal.mm",
]
diff --git a/src/zen/toolkit/common/nsIZenCommonUtils.idl b/src/zen/toolkit/common/nsIZenCommonUtils.idl
index bdfa2ac703..68fa00beca 100644
--- a/src/zen/toolkit/common/nsIZenCommonUtils.idl
+++ b/src/zen/toolkit/common/nsIZenCommonUtils.idl
@@ -8,7 +8,7 @@
/**
* @brief Common utility functions for Zen.
*/
-[scriptable, uuid(d034642a-43b1-4814-be1c-8ad75e337c84)]
+[scriptable, uuid(0670dd29-db27-4cb5-ba6d-eff88cc469b0)]
interface nsIZenCommonUtils : nsISupports {
/*
* @brief Share using the native share dialog.
@@ -32,4 +32,9 @@ interface nsIZenCommonUtils : nsISupports {
* @brief Play a single haptic feedback note if supported.
*/
void playHapticFeedback();
+ /*
+ * @brief Set the macOS app icon from a bundled asset catalog image.
+ * Passing empty strings restores the bundled default icon.
+ */
+ void setMacOSAppIcon(in AString iconBundlePath, in AString iconName);
};