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); };