From 3c014999d8a82921280dd15a1e96ccce1e6cd84b Mon Sep 17 00:00:00 2001 From: "mr. m" Date: Tue, 14 Oct 2025 00:25:13 +0200 Subject: [PATCH] feat: Added copy url button and small tweaks, b=no-bug, c=common, compact-mode, folders, glance --- locales/en-US/browser/browser/zen-general.ftl | 3 + .../urlbar/UrlbarInput-sys-mjs.patch | 16 ++-- src/browser/themes/shared/zen-icons/icons.css | 4 + src/zen/common/ZenUIManager.mjs | 3 + src/zen/common/styles/zen-animations.css | 29 ------ src/zen/common/styles/zen-omnibox.css | 7 +- src/zen/common/styles/zen-panel-ui.css | 6 -- src/zen/common/styles/zen-popup.css | 8 +- .../common/styles/zen-single-components.css | 6 +- src/zen/common/styles/zen-theme.css | 2 +- src/zen/compact-mode/sidebar.inc.css | 1 + .../downloads/zen-download-arc-animation.css | 2 +- .../downloads/zen-download-box-animation.css | 2 +- src/zen/folders/zen-folders.css | 8 +- src/zen/glance/ZenGlanceManager.mjs | 88 +++++++++++++++---- src/zen/glance/actors/ZenGlanceParent.sys.mjs | 35 +------- src/zen/urlbar/ZenSiteDataPanel.sys.mjs | 48 +++++++++- 17 files changed, 158 insertions(+), 110 deletions(-) diff --git a/locales/en-US/browser/browser/zen-general.ftl b/locales/en-US/browser/browser/zen-general.ftl index 37a35a77b..3cf66bfa5 100644 --- a/locales/en-US/browser/browser/zen-general.ftl +++ b/locales/en-US/browser/browser/zen-general.ftl @@ -90,6 +90,9 @@ zen-site-data-get-addons = zen-site-data-site-settings = .label = All Site Settings +zen-urlbar-copy-url-button = + .tooltiptext = Copy URL + zen-site-data-setting-site-protection = Site Protection zen-site-data-panel-feature-callout-title = A new home for add-ons, permissions, and more diff --git a/src/browser/components/urlbar/UrlbarInput-sys-mjs.patch b/src/browser/components/urlbar/UrlbarInput-sys-mjs.patch index 5388f07bc..944fd80e1 100644 --- a/src/browser/components/urlbar/UrlbarInput-sys-mjs.patch +++ b/src/browser/components/urlbar/UrlbarInput-sys-mjs.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/urlbar/UrlbarInput.sys.mjs b/browser/components/urlbar/UrlbarInput.sys.mjs -index afc7a6c6ddbf4cf5a5b27c0bd60577b833c63093..77955d888e70409c83b217e676e1575417018831 100644 +index afc7a6c6ddbf4cf5a5b27c0bd60577b833c63093..a2494f2c0f3e4fc50cbe2fe3bf6e2bd2b69f0cf7 100644 --- a/browser/components/urlbar/UrlbarInput.sys.mjs +++ b/browser/components/urlbar/UrlbarInput.sys.mjs @@ -76,6 +76,13 @@ ChromeUtils.defineLazyGetter(lazy, "logger", () => @@ -157,14 +157,14 @@ index afc7a6c6ddbf4cf5a5b27c0bd60577b833c63093..77955d888e70409c83b217e676e15754 : val; // Only trim value if the directionality doesn't change to RTL and we're not // showing a strikeout https protocol. -@@ -3407,6 +3475,7 @@ export class UrlbarInput { - ); - } - +@@ -3501,6 +3569,7 @@ export class UrlbarInput { + resultDetails = null, + browser = this.window.gBrowser.selectedBrowser + ) { + openUILinkWhere = this.window.gZenUIManager.getOpenUILinkWhere(url, browser, openUILinkWhere); - // No point in setting these because we'll handleRevert() a few rows below. - if (openUILinkWhere == "current") { - // Make sure URL is formatted properly (don't show punycode). + if (this.isAddressbar) { + this.#prepareAddressbarLoad( + url, @@ -3608,6 +3677,10 @@ export class UrlbarInput { } reuseEmpty = true; diff --git a/src/browser/themes/shared/zen-icons/icons.css b/src/browser/themes/shared/zen-icons/icons.css index e5e2aa640..93ecaabdd 100644 --- a/src/browser/themes/shared/zen-icons/icons.css +++ b/src/browser/themes/shared/zen-icons/icons.css @@ -944,3 +944,7 @@ display: none; } } + +#zen-copy-url-button image { + list-style-image: url('link.svg'); +} diff --git a/src/zen/common/ZenUIManager.mjs b/src/zen/common/ZenUIManager.mjs index 509e0aeaf..b19b39f7c 100644 --- a/src/zen/common/ZenUIManager.mjs +++ b/src/zen/common/ZenUIManager.mjs @@ -527,6 +527,9 @@ var gZenUIManager = { this._toastContainer.removeAttribute('hidden'); this._toastContainer.appendChild(toast); const timeoutFunction = () => { + if (Services.prefs.getBoolPref('ui.popup.disable_autohide')) { + return; + } this.motion .animate(toast, { opacity: [1, 0], scale: [1, 0.5] }, { duration: 0.2, bounce: 0 }) .then(() => { diff --git a/src/zen/common/styles/zen-animations.css b/src/zen/common/styles/zen-animations.css index efdc0b6df..ae77e9502 100644 --- a/src/zen/common/styles/zen-animations.css +++ b/src/zen/common/styles/zen-animations.css @@ -3,20 +3,6 @@ * 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/. */ -@keyframes zen-jello-animation { - 0% { - transform: scale3d(0.8, 0.8, 0.8); - } - - 60% { - transform: scale3d(1.02, 1.02, 1.02); - } - - to { - opacity: 1; - transform: scale3d(1, 1, 1); - } -} @keyframes zen-jello-animation-macos { 0% { @@ -30,21 +16,6 @@ } } -@keyframes zen-jello-animation-large { - 0% { - transform: scale3d(0.8, 0.8, 0.8); - } - - 60% { - transform: scale3d(1.02, 1.02, 1.02); - } - - to { - opacity: 1; - transform: scale3d(1, 1, 1); - } -} - @keyframes zen-theme-picker-dot-animation { from { transform: scale(0.8) translate(-50%, -50%); diff --git a/src/zen/common/styles/zen-omnibox.css b/src/zen/common/styles/zen-omnibox.css index 846b60d37..5874345cb 100644 --- a/src/zen/common/styles/zen-omnibox.css +++ b/src/zen/common/styles/zen-omnibox.css @@ -130,7 +130,8 @@ } } - .identity-box-button { + .identity-box-button, + #zen-copy-url-button { opacity: 0; transition: opacity 0.2s, @@ -246,7 +247,9 @@ :root[zen-single-toolbar='true'] { --urlbar-icon-border-radius: 8px !important; - .urlbar-page-action:not([open]):not([showing]):not(#identity-permission-box), + .urlbar-page-action:not([open]):not([showing]):not( + :is(#zen-copy-url-button, #identity-permission-box) + ), #tracking-protection-icon-container { display: none; } diff --git a/src/zen/common/styles/zen-panel-ui.css b/src/zen/common/styles/zen-panel-ui.css index 5badd839e..2c10aea56 100644 --- a/src/zen/common/styles/zen-panel-ui.css +++ b/src/zen/common/styles/zen-panel-ui.css @@ -23,12 +23,6 @@ panel[type='arrow'] { animation: zen-jello-animation-macos 0.2s ease-in-out forwards !important; } } - @media (-moz-platform: linux) or ((-moz-platform: windows) and (not (-moz-windows-mica-popups))) and (-moz-panel-animations) { - /* Mica popups have a weird background while the animation is running */ - &::part(content) { - animation: zen-jello-animation 0.35s ease; - } - } } menupopup, diff --git a/src/zen/common/styles/zen-popup.css b/src/zen/common/styles/zen-popup.css index a6dee2776..a7d95c9b0 100644 --- a/src/zen/common/styles/zen-popup.css +++ b/src/zen/common/styles/zen-popup.css @@ -386,13 +386,17 @@ menuseparator { min-width: unset !important; margin: 0px !important; border-radius: calc(var(--zen-native-inner-radius) + 2px) !important; - background: light-dark(rgba(0, 0, 0, 0.1), rgba(255, 255, 255, 0.1)) !important; - border: 1px solid light-dark(rgba(0, 0, 0, 0.15), rgba(255, 255, 255, 0.15)) !important; + background: light-dark(rgba(0, 0, 0, 0.05), rgba(255, 255, 255, 0.05)) !important; + border: 1px solid light-dark(rgba(0, 0, 0, 0.01), rgba(255, 255, 255, 0.01)) !important; color: light-dark(rgba(0, 0, 0, 0.8), rgba(255, 255, 255, 0.8)) !important; :root[zen-right-side='true'] & { order: -1; } + + & .button-text { + display: none; + } } } } diff --git a/src/zen/common/styles/zen-single-components.css b/src/zen/common/styles/zen-single-components.css index 26ebd8e1f..8108d4aef 100644 --- a/src/zen/common/styles/zen-single-components.css +++ b/src/zen/common/styles/zen-single-components.css @@ -180,7 +180,11 @@ body > #confetti { background-color 0.1s, transform 0.2s; - &:active:hover { + &[open='true'] { + transition: background-color 0.1s; + } + + &:not([open='true']):active:hover { transform: scale(0.95); } } diff --git a/src/zen/common/styles/zen-theme.css b/src/zen/common/styles/zen-theme.css index 2174b75e2..b922c42ea 100644 --- a/src/zen/common/styles/zen-theme.css +++ b/src/zen/common/styles/zen-theme.css @@ -238,7 +238,7 @@ --zen-native-content-radius: env(-moz-gtk-csd-titlebar-radius); } @media (-moz-mac-tahoe-theme) { - --zen-native-content-radius: 14px; + --zen-native-content-radius: 15px; } --zen-native-inner-radius: var( --zen-webview-border-radius, diff --git a/src/zen/compact-mode/sidebar.inc.css b/src/zen/compact-mode/sidebar.inc.css index b5f72008f..e65d4775a 100644 --- a/src/zen/compact-mode/sidebar.inc.css +++ b/src/zen/compact-mode/sidebar.inc.css @@ -39,6 +39,7 @@ #navigator-toolbox { --zen-toolbox-max-width: 74px !important; --zen-compact-float: var(--zen-element-separation); + :root[zen-no-padding='true'] & { --zen-compact-float: 10px; --zen-compact-mode-no-padding-radius-fix: 2px; diff --git a/src/zen/downloads/zen-download-arc-animation.css b/src/zen/downloads/zen-download-arc-animation.css index 0ce56f287..ea3bd2f97 100644 --- a/src/zen/downloads/zen-download-arc-animation.css +++ b/src/zen/downloads/zen-download-arc-animation.css @@ -31,7 +31,7 @@ width: 100%; height: 100%; border-radius: 50%; - background-color: var(--zen-colors-secondary); + background-color: var(--toolbar-color); } .zen-download-arc-animation-icon { diff --git a/src/zen/downloads/zen-download-box-animation.css b/src/zen/downloads/zen-download-box-animation.css index 54634ea9b..211a8091e 100644 --- a/src/zen/downloads/zen-download-box-animation.css +++ b/src/zen/downloads/zen-download-box-animation.css @@ -10,7 +10,7 @@ display: flex; align-items: center; justify-content: center; - background-color: var(--zen-colors-primary); + background-color: var(--toolbar-color); border-radius: var(--zen-native-content-radius); box-shadow: var(--zen-big-shadow); pointer-events: none; diff --git a/src/zen/folders/zen-folders.css b/src/zen/folders/zen-folders.css index 6d4618795..f1be64211 100644 --- a/src/zen/folders/zen-folders.css +++ b/src/zen/folders/zen-folders.css @@ -335,13 +335,13 @@ zen-folder { } &:last-child { - border-bottom-left-radius: max(calc(var(--panel-border-radius) - 6px), 4px); - border-bottom-right-radius: max(calc(var(--panel-border-radius) - 6px), 4px); + border-bottom-left-radius: max(calc(var(--panel-border-radius) - 2px), 4px); + border-bottom-right-radius: max(calc(var(--panel-border-radius) - 2px), 4px); } &:first-child { - border-top-left-radius: max(calc(var(--panel-border-radius) - 6px), 4px); - border-top-right-radius: max(calc(var(--panel-border-radius) - 6px), 4px); + border-top-left-radius: max(calc(var(--panel-border-radius) - 2px), 4px); + border-top-right-radius: max(calc(var(--panel-border-radius) - 2px), 4px); } } diff --git a/src/zen/glance/ZenGlanceManager.mjs b/src/zen/glance/ZenGlanceManager.mjs index fd7eac8b0..5805742fe 100644 --- a/src/zen/glance/ZenGlanceManager.mjs +++ b/src/zen/glance/ZenGlanceManager.mjs @@ -142,7 +142,7 @@ * @param {Tab} existingTab - Optional existing tab to reuse * @returns {Browser} The created browser element */ - createBrowserElement(url, currentTab, existingTab = null) { + #createBrowserElement(url, currentTab, existingTab = null) { const newTabOptions = this.#createTabOptions(currentTab); const newUUID = gZenUIManager.generateUuidv4(); @@ -250,6 +250,28 @@ ); } + /** + * Get element preview data as a data URL + * @param {Object} data - Glance data + * @returns {Promise} Promise resolving to data URL or null + * if not available + */ + async #getElementPreviewData(data) { + // Make the rect relative to the tabpanels. We dont do it directly on the + // content process since it does not take into account scroll. This way, we can + // be sure that the coordinates are correct. + const tabPanelsRect = gBrowser.tabpanels.getBoundingClientRect(); + const rect = new DOMRect( + data.clientX + tabPanelsRect.left, + data.clientY + tabPanelsRect.top, + data.width, + data.height + ); + return await this.#imageBitmapToBase64( + await window.browsingContext.currentWindowGlobal.drawSnapshot(rect, 1, 'transparent', true) + ); + } + /** * Open a glance overlay with the specified data * @param {Object} data - Glance data including URL, position, and dimensions @@ -269,7 +291,7 @@ this.#setAnimationState(true); const currentTab = ownerTab ?? gBrowser.selectedTab; - const browserElement = this.createBrowserElement(data.url, currentTab, existingTab); + const browserElement = this.#createBrowserElement(data.url, currentTab, existingTab); this.fillOverlay(browserElement); this.overlay.classList.add('zen-glance-overlay'); @@ -293,9 +315,13 @@ * @returns {Promise} Promise that resolves to the glance tab */ #animateGlanceOpening(data, browserElement) { - return new Promise((resolve) => { + return new Promise(async (resolve) => { + this.#prepareGlanceAnimation(data, browserElement); + if (data.width && data.height) { + data.elementData = await this.#getElementPreviewData(data); + } + this.#glances.get(this.#currentGlanceID).elementData = data.elementData; window.requestAnimationFrame(() => { - this.#prepareGlanceAnimation(data, browserElement); this.#executeGlanceAnimation(data, browserElement, resolve); }); }); @@ -450,18 +476,24 @@ const transformOrigin = this.#getTransformOrigin(data); this.browserWrapper.style.transformOrigin = transformOrigin; - gZenUIManager.motion - .animate( - this.contentWrapper, - { opacity: [0, 1] }, - { - duration: 0.1, - easing: 'easeInOut', - } - ) - .then(() => { - this.contentWrapper.style.opacity = ''; - }); + + // Only animate if there is element data, so we can apply a + // nice fade-in effect to the content. But if it doesn't exist, + // we just fall back to always showing the browser directly. + if (data.elementData) { + gZenUIManager.motion + .animate( + this.contentWrapper, + { opacity: [0, 1] }, + { + duration: 0.1, + easing: 'easeInOut', + } + ) + .then(() => { + this.contentWrapper.style.opacity = ''; + }); + } gZenUIManager.motion .animate(this.browserWrapper, arcSequence, { @@ -826,6 +858,21 @@ } } + #imageBitmapToBase64(imageBitmap) { + // 1. Create a canvas with the same size as the ImageBitmap + const canvas = document.createElement('canvas'); + canvas.width = imageBitmap.width; + canvas.height = imageBitmap.height; + + // 2. Draw the ImageBitmap onto the canvas + const ctx = canvas.getContext('2d'); + ctx.drawImage(imageBitmap, 0, 0); + + // 3. Convert the canvas content to a Base64 string (PNG by default) + const base64String = canvas.toDataURL('image/png'); + return base64String; + } + /** * Animate parent background restoration * @param {Element} browserSidebarContainer - The sidebar container @@ -1481,10 +1528,15 @@ */ #createGlanceDataFromBookmark(event) { const rect = window.windowUtils.getBoundsWithoutFlushing(event.target); + const tabPanelRect = window.windowUtils.getBoundsWithoutFlushing(gBrowser.tabpanels); + // the bookmark is most likely outisde the tabpanel, so we need to give a negative number + // so it can be corrected later + let top = rect.top - tabPanelRect.top; + let left = rect.left - tabPanelRect.left; return { url: event.target._placesNode.uri, - clientX: rect.left, - clientY: rect.top, + clientX: left, + clientY: top, width: rect.width, height: rect.height, }; diff --git a/src/zen/glance/actors/ZenGlanceParent.sys.mjs b/src/zen/glance/actors/ZenGlanceParent.sys.mjs index 0939cd47a..2bc025df9 100644 --- a/src/zen/glance/actors/ZenGlanceParent.sys.mjs +++ b/src/zen/glance/actors/ZenGlanceParent.sys.mjs @@ -28,38 +28,7 @@ export class ZenGlanceParent extends JSWindowActorParent { } } - #imageBitmapToBase64(imageBitmap) { - // 1. Create a canvas with the same size as the ImageBitmap - const canvas = this.browsingContext.topChromeWindow.document.createElement('canvas'); - canvas.width = imageBitmap.width; - canvas.height = imageBitmap.height; - - // 2. Draw the ImageBitmap onto the canvas - const ctx = canvas.getContext('2d'); - ctx.drawImage(imageBitmap, 0, 0); - - // 3. Convert the canvas content to a Base64 string (PNG by default) - const base64String = canvas.toDataURL('image/png'); - return base64String; - } - - async openGlance(window, data) { - const win = this.browsingContext.topChromeWindow; - const tabPanels = win.gBrowser.tabpanels; - // Make the rect relative to the tabpanels. We dont do it directly on the - // content process since it does not take into account scroll. This way, we can - // be sure that the coordinates are correct. - const tabPanelsRect = tabPanels.getBoundingClientRect(); - const rect = new DOMRect( - data.clientX + tabPanelsRect.left, - data.clientY + tabPanelsRect.top, - data.width, - data.height - ); - const elementData = await this.#imageBitmapToBase64( - await win.browsingContext.currentWindowGlobal.drawSnapshot(rect, 1, 'transparent', true) - ); - data.elementData = elementData; - window.gZenGlanceManager.openGlance(data); + openGlance(window, data) { + return window.gZenGlanceManager.openGlance(data); } } diff --git a/src/zen/urlbar/ZenSiteDataPanel.sys.mjs b/src/zen/urlbar/ZenSiteDataPanel.sys.mjs index a1244d9d5..8d08cfc5e 100644 --- a/src/zen/urlbar/ZenSiteDataPanel.sys.mjs +++ b/src/zen/urlbar/ZenSiteDataPanel.sys.mjs @@ -42,6 +42,7 @@ export class nsZenSiteDataPanel { // Remove the old permissions dialog this.document.getElementById('unified-extensions-panel-template').remove(); + this.#initCopyUrlButton(); this.#initEventListeners(); this.#maybeShowFeatureCallout(); } @@ -65,6 +66,35 @@ export class nsZenSiteDataPanel { this.#initContextMenuEventListener(); } + #initCopyUrlButton() { + // This function is a bit out of place, but it's related enough to the panel + // that it's easier to do it here than in a separate module. + const container = this.document.getElementById('page-action-buttons'); + const fragment = this.window.MozXULElement.parseXULToFragment(` + + `); + container.appendChild(fragment); + + const aElement = this.document.getElementById('zen-copy-url-button'); + aElement.addEventListener('click', (event) => { + this.document.getElementById('cmd_zenCopyCurrentURL').doCommand(); + }); + + this.window.gBrowser.addProgressListener({ + onLocationChange: (aWebProgress, aRequest, aLocation) => { + if (aWebProgress.isTopLevel) { + aElement.hidden = !this.#canCopyUrl(aLocation); + } + }, + }); + } + #initContextMenuEventListener() { const kCommands = { context_zenClearSiteData: (event) => { @@ -117,10 +147,7 @@ export class nsZenSiteDataPanel { } { const button = this.document.getElementById('zen-site-data-header-share'); - if ( - this.window.gBrowser.currentURI.schemeIs('http') || - this.window.gBrowser.currentURI.schemeIs('https') - ) { + if (this.#canCopyUrl(this.window.gBrowser.currentURI)) { button.removeAttribute('disabled'); } else { button.setAttribute('disabled', 'true'); @@ -128,6 +155,19 @@ export class nsZenSiteDataPanel { } } + /* + * Determines whether the copy URL button should be hidden for the given URI. + * @param {nsIURI} uri - The URI to check. + * @returns {boolean} True if the button should be hidden, false otherwise. + */ + #canCopyUrl(uri) { + if (!uri) { + return false; + } + + return uri.scheme.startsWith('http'); + } + #setSiteSecurityInfo() { const { gIdentityHandler } = this.window; const button = this.document.getElementById('zen-site-data-security-info');