From 87e491d742ae29ac13514017d3a0e6621d32e361 Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Tue, 20 May 2025 18:50:54 +0800 Subject: [PATCH 01/34] initial trial of edge scrolling --- .../base/content/zen-assets.jar.inc.mn | 1 + src/zen/common/ZenEdgeScrollFrame.js | 54 ++++ src/zen/common/zen-sets.js | 255 ++++++++++++++++++ 3 files changed, 310 insertions(+) create mode 100644 src/zen/common/ZenEdgeScrollFrame.js diff --git a/src/browser/base/content/zen-assets.jar.inc.mn b/src/browser/base/content/zen-assets.jar.inc.mn index e743a2a4f6..b5fb415400 100644 --- a/src/browser/base/content/zen-assets.jar.inc.mn +++ b/src/browser/base/content/zen-assets.jar.inc.mn @@ -3,6 +3,7 @@ content/browser/zenThemeModifier.js (../../zen/common/zenThemeModifier.js) content/browser/ZenStartup.mjs (../../zen/common/ZenStartup.mjs) content/browser/zen-sets.js (../../zen/common/zen-sets.js) + content/browser/ZenEdgeScrollFrame.js (../../zen/common/ZenEdgeScrollFrame.js) content/browser/ZenUIManager.mjs (../../zen/common/ZenUIManager.mjs) content/browser/zen-components/ZenActorsManager.mjs (../../zen/common/ZenActorsManager.mjs) content/browser/zen-components/ZenEmojies.mjs (../../zen/common/ZenEmojies.mjs) diff --git a/src/zen/common/ZenEdgeScrollFrame.js b/src/zen/common/ZenEdgeScrollFrame.js new file mode 100644 index 0000000000..1c0f57de7a --- /dev/null +++ b/src/zen/common/ZenEdgeScrollFrame.js @@ -0,0 +1,54 @@ +// (Ensure this path is correctly mapped in your chrome.manifest and accessible) +/* eslint-env mozilla/frame-script */ + + +function log(message) { + //dump("ZenEdgeScrollFrame: " + message + "\n"); // Use dump for debugging frame scripts + // Or send a message back to parent for logging if preferred for easier viewing + // sendAsyncMessage("ZenEdgeScroll:Log", { message }); +} + +console.log("Frame script loaded for: " + (content && content.document ? content.document.location.href : "unknown content location")); + +addMessageListener("ZenEdgeScroll:ScrollToPercentage", function(message) { + const doc = content.document; + const scrollableElement = doc.scrollingElement || doc.documentElement || doc.body; + + if (scrollableElement && scrollableElement.scrollHeight > scrollableElement.clientHeight) { + const percentage = message.data.percentage; + const targetScrollTop = percentage * (scrollableElement.scrollHeight - scrollableElement.clientHeight); + scrollableElement.scrollTop = Math.max(0, Math.min(targetScrollTop, scrollableElement.scrollHeight - scrollableElement.clientHeight)); + } else { + console.log("ScrollToPercentage: Content not scrollable or no scrollable element."); + } +}); + +addMessageListener("ZenEdgeScroll:DispatchWheel", function(message) { + console.log("hello2"); + const doc = content.document; + // Dispatch to documentElement, as it's a common target and will bubble. + // Or, could try to find the focused element or element under mouse if more precision is needed. + const targetElement = doc.documentElement; // Or doc.body, or content.document.scrollingElement + + if (targetElement) { + const eventData = message.data.wheelData; + try { + const clonedWheelEvent = new content.WheelEvent("wheel", { // Use content.WheelEvent + deltaX: eventData.deltaX, + deltaY: eventData.deltaY, + deltaZ: eventData.deltaZ, + deltaMode: eventData.deltaMode, + bubbles: true, + cancelable: true, + composed: true, // Important for events crossing shadow DOM boundaries + view: content, // 'content' is the window in a frame script + }); + targetElement.dispatchEvent(clonedWheelEvent); + console.log(`Dispatched wheel event: dY=${eventData.deltaY}`); + } catch (e) { + console.log(`Error dispatching wheel event: ${e} - ${e.stack}`); + } + } else { + console.log("DispatchWheel: No targetElement found."); + } +}); \ No newline at end of file diff --git a/src/zen/common/zen-sets.js b/src/zen/common/zen-sets.js index 1f73018f32..16f071206c 100644 --- a/src/zen/common/zen-sets.js +++ b/src/zen/common/zen-sets.js @@ -108,3 +108,258 @@ document.addEventListener( }, { once: true } ); + +(function() { + if (window.gEdgeScrollHandlerInitialized) return; + window.gEdgeScrollHandlerInitialized = true; + + const FRAME_SCRIPT_URL = "chrome://browser/content/ZenEdgeScrollFrame.js"; // Ensure this URI is correct + const MESSAGE_PREFIX = "ZenEdgeScroll:"; // To namespace messages + + function logParent(message) { + console.log("ZenEdgeScrollParent: " + message); + } + + // Function to load the frame script into a browser's message manager + function loadFrameScriptForBrowser(browser) { + if (browser && browser.messageManager && !browser.frameScriptLoadedForEdgeScroll) { + try { + browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false); // false = don't delay + browser.frameScriptLoadedForEdgeScroll = true; // Custom property to track loading + logParent(`Frame script loading initiated for ${browser.currentURI?.spec || 'browser'}`); + } catch (e) { + console.error("ZenEdgeScrollParent: Error loading frame script:", e, "for URL:", FRAME_SCRIPT_URL); + } + } + } + + // Initialize frame scripts for existing and new tabs + function initFrameScripts() { + if (!gBrowser || !gBrowser.tabs.length) { + logParent("gBrowser not ready or no tabs for initFrameScripts."); + // Retry after a short delay if browser is still initializing + if (!window.gEdgeScrollInitRetry) { + window.gEdgeScrollInitRetry = true; + setTimeout(initFrameScripts, 500); + } + return; + } + + logParent("Initializing frame scripts for tabs..."); + for (const tab of gBrowser.tabs) { + if (tab.linkedBrowser) { + loadFrameScriptForBrowser(tab.linkedBrowser); + } + } + + gBrowser.tabContainer.addEventListener("TabOpen", event => { + const browser = event.target.linkedBrowser; + if (browser) { + loadFrameScriptForBrowser(browser); + } + }); + + // Also load on TabSelect, in case a tab was opened before this script ran fully + // or if a browser is swapped into a tab. + gBrowser.tabContainer.addEventListener("TabSelect", event => { + const browser = event.target.linkedBrowser; + if (browser) { + loadFrameScriptForBrowser(browser); + } + }); + + // Ensure for the initially selected browser + if (gBrowser.selectedBrowser) { + loadFrameScriptForBrowser(gBrowser.selectedBrowser); + } + } + + // Ensure gBrowser is available before initializing + if (document.readyState === "complete") { + initFrameScripts(); + } else { + window.addEventListener("load", initFrameScripts, { once: true }); + } + + const mainBrowserWindow = window; + let isDraggingEdgeScroll = false; + let dragInitialModel = { + mouseY: 0, + targetBrowserDuringDrag: null, + gapZoneHeight: 0, + initialScrollPercentageOnDragStart: 0, + }; + + // --- START: Configuration for edge interaction --- + // How wide (in pixels) the clickable zone at the far right edge of the window is. + const EDGE_INTERACTION_WIDTH_PX = 20; + // How close (in pixels) the active browser's right edge must be to the + // main window's right edge to be considered "rightmost". + const RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX = 15; + // --- END: Configuration for edge interaction --- + + function getGapZoneInfo(event) { + const activeBrowser = gBrowser.selectedBrowser; + if (!activeBrowser) { + // logParent("getGapZoneInfo: No selected/active browser."); + return { isInGap: false, targetBrowser: null, browserRect: null }; + } + + const activeBrowserRect = activeBrowser.getBoundingClientRect(); + const windowWidth = mainBrowserWindow.innerWidth; + + // Condition 1: Is the *active* browser physically the rightmost browser pane? + // Its right edge should be very close to the main window's right edge. + const isActiveBrowserRightmost = + (windowWidth - activeBrowserRect.right) < RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX; + + if (!isActiveBrowserRightmost) { + // logParent(`getGapZoneInfo: Active browser (${activeBrowser.currentURI?.spec}) is not the rightmost. Gap to window edge: ${windowWidth - activeBrowserRect.right}px`); + return { isInGap: false, targetBrowser: null, browserRect: null }; + } + + // Condition 2: Is the mouse event within the far right edge interaction zone of the main window? + const isInFarRightWindowEdgeGap = event.clientX > (windowWidth - EDGE_INTERACTION_WIDTH_PX) && + event.clientX < windowWidth; + + if (isInFarRightWindowEdgeGap) { + // logParent(`getGapZoneInfo: Mouse in far right window edge gap, and active browser (${activeBrowser.currentURI?.spec}) is rightmost.`); + return { + isInGap: true, + targetBrowser: activeBrowser, // The scroll target is the active (and rightmost) browser + browserRect: activeBrowserRect, // The geometry of this target browser + }; + } + + // logParent("getGapZoneInfo: Conditions for edge scroll not met."); + return { isInGap: false, targetBrowser: null, browserRect: null }; + } + + mainBrowserWindow.addEventListener('mousedown', (event) => { + if (event.button !== 0) return; // Only primary button + + const gapInfo = getGapZoneInfo(event); + if (!gapInfo.isInGap || !gapInfo.targetBrowser) { + // logParent("Mousedown: Not in valid gap or no target browser identified by getGapZoneInfo."); + return; + } + + const targetBrowser = gapInfo.targetBrowser; // Browser to act upon + + if (!targetBrowser.messageManager) { + logParent("Mousedown: No messageManager for target browser."); + return; + } + loadFrameScriptForBrowser(targetBrowser); // Ensure frame script is loaded for this specific browser + + event.preventDefault(); + isDraggingEdgeScroll = true; + + dragInitialModel.targetBrowserDuringDrag = targetBrowser; + dragInitialModel.gapZoneHeight = gapInfo.browserRect.height; // Use the rect of the targetBrowser + dragInitialModel.mouseY = event.clientY; + + const clickYInGap = event.clientY - gapInfo.browserRect.top; + const scrollPercentage = dragInitialModel.gapZoneHeight > 0 ? Math.max(0, Math.min(1, clickYInGap / dragInitialModel.gapZoneHeight)) : 0; + dragInitialModel.initialScrollPercentageOnDragStart = scrollPercentage; + + // logParent(`Mousedown: Sending ScrollToPercentage ${scrollPercentage} to ${targetBrowser.currentURI?.spec}`); + targetBrowser.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "ScrollToPercentage", { percentage: scrollPercentage }); + + mainBrowserWindow.addEventListener('mousemove', handleEdgeScrollDrag); + mainBrowserWindow.addEventListener('mouseup', handleEdgeScrollEnd); + }, true); + + function handleEdgeScrollDrag(event) { + if (!isDraggingEdgeScroll || !dragInitialModel.targetBrowserDuringDrag) return; + + const targetBrowser = dragInitialModel.targetBrowserDuringDrag; + + if (gBrowser.selectedBrowser !== targetBrowser || // Also ensure the target is still the active one during drag + !targetBrowser.messageManager || + !targetBrowser.contentWindow) { // Added check for contentWindow + // logParent("Drag: Target browser changed, lost messageManager, or no contentWindow. Ending drag."); + handleEdgeScrollEnd(event); // Clean up + return; + } + event.preventDefault(); + + const deltaYFromInitialMouseY = event.clientY - dragInitialModel.mouseY; + let newScrollPercentage; + + if (dragInitialModel.gapZoneHeight > 0) { + const percentageDelta = deltaYFromInitialMouseY / dragInitialModel.gapZoneHeight; + newScrollPercentage = dragInitialModel.initialScrollPercentageOnDragStart + percentageDelta; + newScrollPercentage = Math.max(0, Math.min(1, newScrollPercentage)); + } else { + return; // Should not happen if drag started correctly + } + + targetBrowser.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "ScrollToPercentage", { percentage: newScrollPercentage }); + } + + function handleEdgeScrollDrag(event) { + if (!isDraggingEdgeScroll || !dragInitialModel.targetBrowserDuringDrag) return; + + if (gBrowser.selectedBrowser !== dragInitialModel.targetBrowserDuringDrag || !dragInitialModel.targetBrowserDuringDrag.messageManager) { + logParent("Drag: Target browser changed or messageManager lost. Ending drag."); + handleEdgeScrollEnd(event); + return; + } + event.preventDefault(); + + const deltaYFromInitialMouseY = event.clientY - dragInitialModel.mouseY; + let newScrollPercentage; + + if (dragInitialModel.gapZoneHeight > 0) { + const percentageDelta = deltaYFromInitialMouseY / dragInitialModel.gapZoneHeight; + newScrollPercentage = dragInitialModel.initialScrollPercentageOnDragStart + percentageDelta; + newScrollPercentage = Math.max(0, Math.min(1, newScrollPercentage)); + } else { + // Fallback if gapZoneHeight was 0, though unlikely if drag started. + // In this case, we might not be able to calculate a meaningful percentage. + // For simplicity, we'll just not scroll further if gapZoneHeight is 0. + return; + } + + // logParent(`Drag: Sending ScrollToPercentage ${newScrollPercentage}`); + dragInitialModel.targetBrowserDuringDrag.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "ScrollToPercentage", { percentage: newScrollPercentage }); + } + + function handleEdgeScrollEnd(event) { + if (event && event.button !== 0 && isDraggingEdgeScroll) return; + + isDraggingEdgeScroll = false; + dragInitialModel.targetBrowserDuringDrag = null; + mainBrowserWindow.removeEventListener('mousemove', handleEdgeScrollDrag); + mainBrowserWindow.removeEventListener('mouseup', handleEdgeScrollEnd); + logParent("DragEnd: Event listeners removed."); + } + + mainBrowserWindow.addEventListener('wheel', (event) => { + const gapInfo = getGapZoneInfo(event); + if (!gapInfo || !gapInfo.isInGap) return; + + const currentBrowser = gBrowser.selectedBrowser; + if (!currentBrowser || !currentBrowser.messageManager) { + logParent("Wheel: No selected browser or messageManager."); + return; + } + loadFrameScriptForBrowser(currentBrowser); + + + event.preventDefault(); + event.stopPropagation(); + + const wheelData = { + deltaX: event.deltaX, + deltaY: event.deltaY, + deltaZ: event.deltaZ, + deltaMode: event.deltaMode, + }; + logParent(`Wheel: Sending DispatchWheel dY=${wheelData.deltaY}`); + currentBrowser.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "DispatchWheel", { wheelData }); + }, { capture: true, passive: false }); + + logParent("Edge Scroll Handler Initialized (IPC version)"); +})(); \ No newline at end of file From e5a30c1ae3359c5e6bb2078181a7795925cae2a0 Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Wed, 21 May 2025 00:24:30 +0800 Subject: [PATCH 02/34] Try to change implementation into fake mouse event but failed --- .../base/content/zen-assets.jar.inc.mn | 1 + src/zen/common/ZenEdgeScrollFrame.js | 104 ++++--- src/zen/common/ZenEdgeScrollHandler.js | 244 +++++++++++++++++ src/zen/common/ZenStartup.mjs | 13 + src/zen/common/zen-sets.js | 257 +----------------- 5 files changed, 329 insertions(+), 290 deletions(-) create mode 100644 src/zen/common/ZenEdgeScrollHandler.js diff --git a/src/browser/base/content/zen-assets.jar.inc.mn b/src/browser/base/content/zen-assets.jar.inc.mn index b5fb415400..9abca3bc03 100644 --- a/src/browser/base/content/zen-assets.jar.inc.mn +++ b/src/browser/base/content/zen-assets.jar.inc.mn @@ -3,6 +3,7 @@ content/browser/zenThemeModifier.js (../../zen/common/zenThemeModifier.js) content/browser/ZenStartup.mjs (../../zen/common/ZenStartup.mjs) content/browser/zen-sets.js (../../zen/common/zen-sets.js) + content/browser/ZenEdgeScrollHandler.js (../../zen/common/ZenEdgeScrollHandler.js) content/browser/ZenEdgeScrollFrame.js (../../zen/common/ZenEdgeScrollFrame.js) content/browser/ZenUIManager.mjs (../../zen/common/ZenUIManager.mjs) content/browser/zen-components/ZenActorsManager.mjs (../../zen/common/ZenActorsManager.mjs) diff --git a/src/zen/common/ZenEdgeScrollFrame.js b/src/zen/common/ZenEdgeScrollFrame.js index 1c0f57de7a..4917a112fc 100644 --- a/src/zen/common/ZenEdgeScrollFrame.js +++ b/src/zen/common/ZenEdgeScrollFrame.js @@ -1,54 +1,90 @@ -// (Ensure this path is correctly mapped in your chrome.manifest and accessible) /* eslint-env mozilla/frame-script */ - -function log(message) { - //dump("ZenEdgeScrollFrame: " + message + "\n"); // Use dump for debugging frame scripts - // Or send a message back to parent for logging if preferred for easier viewing - // sendAsyncMessage("ZenEdgeScroll:Log", { message }); +function logFrame(message) { + dump("ZenEdgeScrollFrame: " + message + "\n"); } -console.log("Frame script loaded for: " + (content && content.document ? content.document.location.href : "unknown content location")); +logFrame("Frame script loaded for: " + (content && content.document ? content.document.location.href : "unknown content location")); + +addMessageListener("ZenEdgeScroll:SynthesizeMouseEvent", function(message) { + const data = message.data; + if (!data || !data.type) { + logFrame("SynthesizeMouseEvent: Invalid data received."); + return; + } -addMessageListener("ZenEdgeScroll:ScrollToPercentage", function(message) { - const doc = content.document; - const scrollableElement = doc.scrollingElement || doc.documentElement || doc.body; + // clientX, clientY are relative to the content viewport + const targetElement = content.document.elementFromPoint(data.clientX, data.clientY) || content.document.documentElement; - if (scrollableElement && scrollableElement.scrollHeight > scrollableElement.clientHeight) { - const percentage = message.data.percentage; - const targetScrollTop = percentage * (scrollableElement.scrollHeight - scrollableElement.clientHeight); - scrollableElement.scrollTop = Math.max(0, Math.min(targetScrollTop, scrollableElement.scrollHeight - scrollableElement.clientHeight)); + if (targetElement) { + try { + const syntheticEvent = new content.MouseEvent(data.type, { + bubbles: true, + cancelable: (data.type !== 'mousemove'), // mousemove is often not cancelable by default + composed: true, + view: content, + detail: (data.type === 'mousedown' || data.type === 'mouseup' || data.type === 'click') ? 1 : 0, // click count + screenX: data.screenX, + screenY: data.screenY, + clientX: data.clientX, + clientY: data.clientY, + ctrlKey: data.ctrlKey, + altKey: data.altKey, + shiftKey: data.shiftKey, + metaKey: data.metaKey, + button: data.button, + buttons: data.buttons, + }); + targetElement.dispatchEvent(syntheticEvent); + // logFrame(`Dispatched synthetic ${data.type} at (${data.clientX}, ${data.clientY}) on ${targetElement.tagName}`); + } catch (e) { + logFrame(`Error dispatching synthetic ${data.type}: ${e} - ${e.stack}`); + } } else { - console.log("ScrollToPercentage: Content not scrollable or no scrollable element."); + logFrame(`SynthesizeMouseEvent: No target element found at (${data.clientX}, ${data.clientY})`); } }); -addMessageListener("ZenEdgeScroll:DispatchWheel", function(message) { - console.log("hello2"); - const doc = content.document; - // Dispatch to documentElement, as it's a common target and will bubble. - // Or, could try to find the focused element or element under mouse if more precision is needed. - const targetElement = doc.documentElement; // Or doc.body, or content.document.scrollingElement +addMessageListener("ZenEdgeScroll:SynthesizeWheelEvent", function(message) { + const data = message.data; + if (!data) { + logFrame("SynthesizeWheelEvent: Invalid data received."); + return; + } + + const targetElement = content.document.elementFromPoint(data.clientX, data.clientY) || content.document.documentElement; if (targetElement) { - const eventData = message.data.wheelData; try { - const clonedWheelEvent = new content.WheelEvent("wheel", { // Use content.WheelEvent - deltaX: eventData.deltaX, - deltaY: eventData.deltaY, - deltaZ: eventData.deltaZ, - deltaMode: eventData.deltaMode, + const syntheticEvent = new content.WheelEvent('wheel', { // type is always 'wheel' bubbles: true, cancelable: true, - composed: true, // Important for events crossing shadow DOM boundaries - view: content, // 'content' is the window in a frame script + composed: true, + view: content, + screenX: data.screenX, + screenY: data.screenY, + clientX: data.clientX, + clientY: data.clientY, + ctrlKey: data.ctrlKey, + altKey: data.altKey, + shiftKey: data.shiftKey, + metaKey: data.metaKey, + button: data.button, // Usually 0 for wheel + buttons: data.buttons, + deltaX: data.deltaX, + deltaY: data.deltaY, + deltaZ: data.deltaZ, + deltaMode: data.deltaMode, }); - targetElement.dispatchEvent(clonedWheelEvent); - console.log(`Dispatched wheel event: dY=${eventData.deltaY}`); + targetElement.dispatchEvent(syntheticEvent); + // logFrame(`Dispatched synthetic wheel at (${data.clientX}, ${data.clientY}) on ${targetElement.tagName}`); } catch (e) { - console.log(`Error dispatching wheel event: ${e} - ${e.stack}`); + logFrame(`Error dispatching synthetic wheel event: ${e} - ${e.stack}`); } } else { - console.log("DispatchWheel: No targetElement found."); + logFrame(`SynthesizeWheelEvent: No target element found at (${data.clientX}, ${data.clientY})`); } -}); \ No newline at end of file +}); + +// Remove the old ScrollToPercentage listener if it's no longer needed +// addMessageListener("ZenEdgeScroll:ScrollToPercentage", function(message) { /* ... */ }); \ No newline at end of file diff --git a/src/zen/common/ZenEdgeScrollHandler.js b/src/zen/common/ZenEdgeScrollHandler.js new file mode 100644 index 0000000000..a0f16434c0 --- /dev/null +++ b/src/zen/common/ZenEdgeScrollHandler.js @@ -0,0 +1,244 @@ +// Suggested filepath: /home/stnav/Projects/others/zen-desktop/src/zen/ui/EdgeScrollHandler.js +// (Or any other suitable path within your project for browser chrome scripts) + +(function() { + if (window.gEdgeScrollHandlerInitialized) { + console.log("EdgeScrollHandler: Already initialized."); + return; + } + window.gEdgeScrollHandlerInitialized = true; + + const FRAME_SCRIPT_URL = "chrome://browser/content/ZenEdgeScrollFrame.js"; // Ensure this matches your frame script's packaged URI + const MESSAGE_PREFIX = "ZenEdgeScroll:"; + + // --- START: Configuration for edge interaction --- + const EDGE_INTERACTION_WIDTH_PX = 20; // How wide the clickable zone at the far right edge of the window is. + const RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX = 25; // Increased slightly for more tolerance with window borders/shadows + const SYNTHETIC_EVENT_OFFSET_FROM_EDGE = 3; // How many pixels inside the content edge to target (e.g., for scrollbar thumb) + // --- END: Configuration for edge interaction --- + + const mainBrowserWindow = window; // 'window' in this context is the main browser window + let isSynthesizingDrag = false; + let dragInitialModel = { + targetBrowserDuringDrag: null, + // No scroll-specific properties needed here anymore + }; + + function logParent(message) { + // console.log("EdgeScrollHandler (Parent): " + message); + // dump("EdgeScrollHandler (Parent): " + message + "\n"); // For terminal output + } + + // Function to load the frame script into a browser's message manager + function loadFrameScriptForBrowser(browser) { + if (browser && browser.messageManager && !browser.frameScriptLoadedForEdgeScroll) { + try { + logParent(`Attempting to load frame script: ${FRAME_SCRIPT_URL} for browser: ${browser.currentURI?.spec}`); + browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false); // false = don't delay + browser.frameScriptLoadedForEdgeScroll = true; // Custom property to track loading + logParent(`Frame script loading initiated for ${browser.currentURI?.spec || 'browser'}.`); + } catch (e) { + console.error("EdgeScrollHandler (Parent): CRITICAL ERROR loading frame script:", FRAME_SCRIPT_URL, e, "for URL:", browser.currentURI?.spec); + dump(`EdgeScrollHandler (Parent): CRITICAL ERROR loading frame script: ${FRAME_SCRIPT_URL} - ${e} - ${e.stack}\n`); + } + } else if (browser && browser.frameScriptLoadedForEdgeScroll) { + // logParent(`Frame script already marked as loaded for ${browser.currentURI?.spec}`); + } else if (!browser) { + logParent("loadFrameScriptForBrowser: browser is null"); + } else if (!browser.messageManager) { + logParent(`loadFrameScriptForBrowser: browser.messageManager is null for ${browser.currentURI?.spec}`); + } + } + + // Initialize frame scripts for existing and new tabs + function initFrameScripts() { + if (!gBrowser || typeof gBrowser.tabs === 'undefined' || !gBrowser.tabs.length) { + logParent("gBrowser not ready or no tabs for initFrameScripts."); + if (!window.gEdgeScrollInitRetryCount || window.gEdgeScrollInitRetryCount < 5) { // Limit retries + window.gEdgeScrollInitRetryCount = (window.gEdgeScrollInitRetryCount || 0) + 1; + setTimeout(initFrameScripts, 500 * window.gEdgeScrollInitRetryCount); + } + return; + } + + logParent("Initializing frame scripts for tabs..."); + for (const tab of gBrowser.tabs) { + if (tab.linkedBrowser) { + loadFrameScriptForBrowser(tab.linkedBrowser); + } + } + + gBrowser.tabContainer.addEventListener("TabOpen", event => { + const browser = event.target.linkedBrowser; + if (browser) { + // Frame scripts are often loaded on demand or when the browser is ready + // Listening for 'load' or 'DOMContentLoaded' on the browser might be more robust + // For now, direct load on TabOpen + loadFrameScriptForBrowser(browser); + } + }); + + gBrowser.tabContainer.addEventListener("TabSelect", event => { + const browser = event.target.linkedBrowser; + if (browser) { + loadFrameScriptForBrowser(browser); + } + }); + + if (gBrowser.selectedBrowser) { + loadFrameScriptForBrowser(gBrowser.selectedBrowser); + } + } + + + function getGapZoneInfo(event) { + const activeBrowser = gBrowser.selectedBrowser; + if (!activeBrowser) { + return { isInGap: false, targetBrowser: null, browserRect: null }; + } + + const activeBrowserRect = activeBrowser.getBoundingClientRect(); + const windowWidth = mainBrowserWindow.innerWidth; + + const isActiveBrowserRightmost = + (windowWidth - activeBrowserRect.right) < RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX; + + if (!isActiveBrowserRightmost) { + return { isInGap: false, targetBrowser: null, browserRect: null }; + } + + const isInFarRightWindowEdgeGap = event.clientX > (windowWidth - EDGE_INTERACTION_WIDTH_PX) && + event.clientX < windowWidth; + + if (isInFarRightWindowEdgeGap) { + return { + isInGap: true, + targetBrowser: activeBrowser, + browserRect: activeBrowserRect, + }; + } + return { isInGap: false, targetBrowser: null, browserRect: null }; + } + + function createSyntheticEventData(originalEvent, targetBrowserRect, eventType) { + const clientXInContent = Math.max(0, Math.floor(targetBrowserRect.width - SYNTHETIC_EVENT_OFFSET_FROM_EDGE)); + const clientYInContent = Math.max(0, Math.min(Math.floor(originalEvent.clientY - targetBrowserRect.top), Math.floor(targetBrowserRect.height - 1))); + + const screenX = Math.floor(mainBrowserWindow.screenX + targetBrowserRect.left + clientXInContent); + const screenY = Math.floor(mainBrowserWindow.screenY + targetBrowserRect.top + clientYInContent); + + return { + type: eventType, + clientX: clientXInContent, + clientY: clientYInContent, + screenX: screenX, + screenY: screenY, + button: originalEvent.button, + buttons: originalEvent.buttons, + ctrlKey: originalEvent.ctrlKey, + altKey: originalEvent.altKey, + shiftKey: originalEvent.shiftKey, + metaKey: originalEvent.metaKey, + deltaX: originalEvent.deltaX || 0, // Ensure these exist for wheel + deltaY: originalEvent.deltaY || 0, + deltaZ: originalEvent.deltaZ || 0, + deltaMode: originalEvent.deltaMode || 0, + }; + } + + mainBrowserWindow.addEventListener('mousedown', (event) => { + if (event.button !== 0) return; + + const gapInfo = getGapZoneInfo(event); + if (!gapInfo.isInGap || !gapInfo.targetBrowser) return; + + const targetBrowser = gapInfo.targetBrowser; + if (!targetBrowser.messageManager) { + logParent("Mousedown: No messageManager for target browser."); + return; + } + loadFrameScriptForBrowser(targetBrowser); + + event.preventDefault(); + isSynthesizingDrag = true; + dragInitialModel.targetBrowserDuringDrag = targetBrowser; + + const eventData = createSyntheticEventData(event, gapInfo.browserRect, 'mousedown'); + targetBrowser.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "SynthesizeMouseEvent", eventData); + + mainBrowserWindow.addEventListener('mousemove', handleSyntheticDrag, true); // Use capture for mousemove + mainBrowserWindow.addEventListener('mouseup', handleSyntheticDragEnd, true); // Use capture for mouseup + }, true); // Use capture for mousedown + + function handleSyntheticDrag(event) { + if (!isSynthesizingDrag || !dragInitialModel.targetBrowserDuringDrag) return; + + const targetBrowser = dragInitialModel.targetBrowserDuringDrag; + if (gBrowser.selectedBrowser !== targetBrowser || !targetBrowser.messageManager || !targetBrowser.contentWindow) { + logParent("Drag: Target browser changed, lost messageManager, or no contentWindow. Ending drag."); + handleSyntheticDragEnd(event); + return; + } + event.preventDefault(); + event.stopPropagation(); // Stop event from bubbling further in parent + + const targetBrowserRect = targetBrowser.getBoundingClientRect(); + const eventData = createSyntheticEventData(event, targetBrowserRect, 'mousemove'); + targetBrowser.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "SynthesizeMouseEvent", eventData); + } + + function handleSyntheticDragEnd(event) { + if (isSynthesizingDrag && dragInitialModel.targetBrowserDuringDrag) { + const targetBrowser = dragInitialModel.targetBrowserDuringDrag; + if (targetBrowser.messageManager) { + // Ensure event is not null if called without one (e.g. from a cleanup path) + if (event) { + event.preventDefault(); + event.stopPropagation(); + const targetBrowserRect = targetBrowser.getBoundingClientRect(); + const eventData = createSyntheticEventData(event, targetBrowserRect, 'mouseup'); + targetBrowser.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "SynthesizeMouseEvent", eventData); + } else { + // If event is null, we might not have coordinates for mouseup. + // Depending on desired behavior, could send a generic mouseup or skip. + // For now, we only send if we have an event. + logParent("DragEnd: Called without an event, mouseup not synthesized to content."); + } + } + } + + isSynthesizingDrag = false; + dragInitialModel.targetBrowserDuringDrag = null; + mainBrowserWindow.removeEventListener('mousemove', handleSyntheticDrag, true); + mainBrowserWindow.removeEventListener('mouseup', handleSyntheticDragEnd, true); + // logParent("DragEnd: Event listeners removed."); + } + + mainBrowserWindow.addEventListener('wheel', (event) => { + const gapInfo = getGapZoneInfo(event); + if (!gapInfo.isInGap || !gapInfo.targetBrowser) return; + + const targetBrowser = gapInfo.targetBrowser; + if (!targetBrowser.messageManager) { + logParent("Wheel: No messageManager for target browser."); + return; + } + loadFrameScriptForBrowser(targetBrowser); + + event.preventDefault(); + event.stopPropagation(); + + const eventData = createSyntheticEventData(event, gapInfo.browserRect, 'wheel'); // type 'wheel' is just for our data + targetBrowser.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "SynthesizeWheelEvent", eventData); + }, { capture: true, passive: false }); + + // Initialize frame script loading logic + // Ensure gBrowser is available before initializing + if (document.readyState === "complete" || document.readyState === "interactive") { + initFrameScripts(); + } else { + mainBrowserWindow.addEventListener("load", initFrameScripts, { once: true }); + } + + logParent("Edge Scroll Handler Initialized (IPC for Synthetic Events)"); +})(); \ No newline at end of file diff --git a/src/zen/common/ZenStartup.mjs b/src/zen/common/ZenStartup.mjs index 73fa6882e0..30ec0bbcb8 100644 --- a/src/zen/common/ZenStartup.mjs +++ b/src/zen/common/ZenStartup.mjs @@ -35,6 +35,19 @@ gZenVerticalTabsManager.init(); gZenUIManager.init(); + // --- Load EdgeScrollHandler.js --- + try { + // Assuming EdgeScrollHandler.js is packaged to be accessible via this URI + // and your chrome.manifest maps chrome://zen/content/ui/... correctly. + const edgeScrollHandlerURL = "chrome://browser/content/ZenEdgeScrollHandler.js"; + Services.scriptloader.loadSubScript(edgeScrollHandlerURL, window, "UTF-8"); + console.log("ZenStartup: EdgeScrollHandler.js loaded successfully."); + } catch (e) { + console.error("ZenStartup: Failed to load EdgeScrollHandler.js:", e); + dump("ZenStartup: Failed to load EdgeScrollHandler.js: " + e + "\n" + (e.stack || "") + "\n"); + } + // --- End Load EdgeScrollHandler.js --- + this._checkForWelcomePage(); document.l10n.setAttributes( diff --git a/src/zen/common/zen-sets.js b/src/zen/common/zen-sets.js index 16f071206c..8b7c5e5dfa 100644 --- a/src/zen/common/zen-sets.js +++ b/src/zen/common/zen-sets.js @@ -107,259 +107,4 @@ document.addEventListener( }); }, { once: true } -); - -(function() { - if (window.gEdgeScrollHandlerInitialized) return; - window.gEdgeScrollHandlerInitialized = true; - - const FRAME_SCRIPT_URL = "chrome://browser/content/ZenEdgeScrollFrame.js"; // Ensure this URI is correct - const MESSAGE_PREFIX = "ZenEdgeScroll:"; // To namespace messages - - function logParent(message) { - console.log("ZenEdgeScrollParent: " + message); - } - - // Function to load the frame script into a browser's message manager - function loadFrameScriptForBrowser(browser) { - if (browser && browser.messageManager && !browser.frameScriptLoadedForEdgeScroll) { - try { - browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false); // false = don't delay - browser.frameScriptLoadedForEdgeScroll = true; // Custom property to track loading - logParent(`Frame script loading initiated for ${browser.currentURI?.spec || 'browser'}`); - } catch (e) { - console.error("ZenEdgeScrollParent: Error loading frame script:", e, "for URL:", FRAME_SCRIPT_URL); - } - } - } - - // Initialize frame scripts for existing and new tabs - function initFrameScripts() { - if (!gBrowser || !gBrowser.tabs.length) { - logParent("gBrowser not ready or no tabs for initFrameScripts."); - // Retry after a short delay if browser is still initializing - if (!window.gEdgeScrollInitRetry) { - window.gEdgeScrollInitRetry = true; - setTimeout(initFrameScripts, 500); - } - return; - } - - logParent("Initializing frame scripts for tabs..."); - for (const tab of gBrowser.tabs) { - if (tab.linkedBrowser) { - loadFrameScriptForBrowser(tab.linkedBrowser); - } - } - - gBrowser.tabContainer.addEventListener("TabOpen", event => { - const browser = event.target.linkedBrowser; - if (browser) { - loadFrameScriptForBrowser(browser); - } - }); - - // Also load on TabSelect, in case a tab was opened before this script ran fully - // or if a browser is swapped into a tab. - gBrowser.tabContainer.addEventListener("TabSelect", event => { - const browser = event.target.linkedBrowser; - if (browser) { - loadFrameScriptForBrowser(browser); - } - }); - - // Ensure for the initially selected browser - if (gBrowser.selectedBrowser) { - loadFrameScriptForBrowser(gBrowser.selectedBrowser); - } - } - - // Ensure gBrowser is available before initializing - if (document.readyState === "complete") { - initFrameScripts(); - } else { - window.addEventListener("load", initFrameScripts, { once: true }); - } - - const mainBrowserWindow = window; - let isDraggingEdgeScroll = false; - let dragInitialModel = { - mouseY: 0, - targetBrowserDuringDrag: null, - gapZoneHeight: 0, - initialScrollPercentageOnDragStart: 0, - }; - - // --- START: Configuration for edge interaction --- - // How wide (in pixels) the clickable zone at the far right edge of the window is. - const EDGE_INTERACTION_WIDTH_PX = 20; - // How close (in pixels) the active browser's right edge must be to the - // main window's right edge to be considered "rightmost". - const RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX = 15; - // --- END: Configuration for edge interaction --- - - function getGapZoneInfo(event) { - const activeBrowser = gBrowser.selectedBrowser; - if (!activeBrowser) { - // logParent("getGapZoneInfo: No selected/active browser."); - return { isInGap: false, targetBrowser: null, browserRect: null }; - } - - const activeBrowserRect = activeBrowser.getBoundingClientRect(); - const windowWidth = mainBrowserWindow.innerWidth; - - // Condition 1: Is the *active* browser physically the rightmost browser pane? - // Its right edge should be very close to the main window's right edge. - const isActiveBrowserRightmost = - (windowWidth - activeBrowserRect.right) < RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX; - - if (!isActiveBrowserRightmost) { - // logParent(`getGapZoneInfo: Active browser (${activeBrowser.currentURI?.spec}) is not the rightmost. Gap to window edge: ${windowWidth - activeBrowserRect.right}px`); - return { isInGap: false, targetBrowser: null, browserRect: null }; - } - - // Condition 2: Is the mouse event within the far right edge interaction zone of the main window? - const isInFarRightWindowEdgeGap = event.clientX > (windowWidth - EDGE_INTERACTION_WIDTH_PX) && - event.clientX < windowWidth; - - if (isInFarRightWindowEdgeGap) { - // logParent(`getGapZoneInfo: Mouse in far right window edge gap, and active browser (${activeBrowser.currentURI?.spec}) is rightmost.`); - return { - isInGap: true, - targetBrowser: activeBrowser, // The scroll target is the active (and rightmost) browser - browserRect: activeBrowserRect, // The geometry of this target browser - }; - } - - // logParent("getGapZoneInfo: Conditions for edge scroll not met."); - return { isInGap: false, targetBrowser: null, browserRect: null }; - } - - mainBrowserWindow.addEventListener('mousedown', (event) => { - if (event.button !== 0) return; // Only primary button - - const gapInfo = getGapZoneInfo(event); - if (!gapInfo.isInGap || !gapInfo.targetBrowser) { - // logParent("Mousedown: Not in valid gap or no target browser identified by getGapZoneInfo."); - return; - } - - const targetBrowser = gapInfo.targetBrowser; // Browser to act upon - - if (!targetBrowser.messageManager) { - logParent("Mousedown: No messageManager for target browser."); - return; - } - loadFrameScriptForBrowser(targetBrowser); // Ensure frame script is loaded for this specific browser - - event.preventDefault(); - isDraggingEdgeScroll = true; - - dragInitialModel.targetBrowserDuringDrag = targetBrowser; - dragInitialModel.gapZoneHeight = gapInfo.browserRect.height; // Use the rect of the targetBrowser - dragInitialModel.mouseY = event.clientY; - - const clickYInGap = event.clientY - gapInfo.browserRect.top; - const scrollPercentage = dragInitialModel.gapZoneHeight > 0 ? Math.max(0, Math.min(1, clickYInGap / dragInitialModel.gapZoneHeight)) : 0; - dragInitialModel.initialScrollPercentageOnDragStart = scrollPercentage; - - // logParent(`Mousedown: Sending ScrollToPercentage ${scrollPercentage} to ${targetBrowser.currentURI?.spec}`); - targetBrowser.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "ScrollToPercentage", { percentage: scrollPercentage }); - - mainBrowserWindow.addEventListener('mousemove', handleEdgeScrollDrag); - mainBrowserWindow.addEventListener('mouseup', handleEdgeScrollEnd); - }, true); - - function handleEdgeScrollDrag(event) { - if (!isDraggingEdgeScroll || !dragInitialModel.targetBrowserDuringDrag) return; - - const targetBrowser = dragInitialModel.targetBrowserDuringDrag; - - if (gBrowser.selectedBrowser !== targetBrowser || // Also ensure the target is still the active one during drag - !targetBrowser.messageManager || - !targetBrowser.contentWindow) { // Added check for contentWindow - // logParent("Drag: Target browser changed, lost messageManager, or no contentWindow. Ending drag."); - handleEdgeScrollEnd(event); // Clean up - return; - } - event.preventDefault(); - - const deltaYFromInitialMouseY = event.clientY - dragInitialModel.mouseY; - let newScrollPercentage; - - if (dragInitialModel.gapZoneHeight > 0) { - const percentageDelta = deltaYFromInitialMouseY / dragInitialModel.gapZoneHeight; - newScrollPercentage = dragInitialModel.initialScrollPercentageOnDragStart + percentageDelta; - newScrollPercentage = Math.max(0, Math.min(1, newScrollPercentage)); - } else { - return; // Should not happen if drag started correctly - } - - targetBrowser.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "ScrollToPercentage", { percentage: newScrollPercentage }); - } - - function handleEdgeScrollDrag(event) { - if (!isDraggingEdgeScroll || !dragInitialModel.targetBrowserDuringDrag) return; - - if (gBrowser.selectedBrowser !== dragInitialModel.targetBrowserDuringDrag || !dragInitialModel.targetBrowserDuringDrag.messageManager) { - logParent("Drag: Target browser changed or messageManager lost. Ending drag."); - handleEdgeScrollEnd(event); - return; - } - event.preventDefault(); - - const deltaYFromInitialMouseY = event.clientY - dragInitialModel.mouseY; - let newScrollPercentage; - - if (dragInitialModel.gapZoneHeight > 0) { - const percentageDelta = deltaYFromInitialMouseY / dragInitialModel.gapZoneHeight; - newScrollPercentage = dragInitialModel.initialScrollPercentageOnDragStart + percentageDelta; - newScrollPercentage = Math.max(0, Math.min(1, newScrollPercentage)); - } else { - // Fallback if gapZoneHeight was 0, though unlikely if drag started. - // In this case, we might not be able to calculate a meaningful percentage. - // For simplicity, we'll just not scroll further if gapZoneHeight is 0. - return; - } - - // logParent(`Drag: Sending ScrollToPercentage ${newScrollPercentage}`); - dragInitialModel.targetBrowserDuringDrag.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "ScrollToPercentage", { percentage: newScrollPercentage }); - } - - function handleEdgeScrollEnd(event) { - if (event && event.button !== 0 && isDraggingEdgeScroll) return; - - isDraggingEdgeScroll = false; - dragInitialModel.targetBrowserDuringDrag = null; - mainBrowserWindow.removeEventListener('mousemove', handleEdgeScrollDrag); - mainBrowserWindow.removeEventListener('mouseup', handleEdgeScrollEnd); - logParent("DragEnd: Event listeners removed."); - } - - mainBrowserWindow.addEventListener('wheel', (event) => { - const gapInfo = getGapZoneInfo(event); - if (!gapInfo || !gapInfo.isInGap) return; - - const currentBrowser = gBrowser.selectedBrowser; - if (!currentBrowser || !currentBrowser.messageManager) { - logParent("Wheel: No selected browser or messageManager."); - return; - } - loadFrameScriptForBrowser(currentBrowser); - - - event.preventDefault(); - event.stopPropagation(); - - const wheelData = { - deltaX: event.deltaX, - deltaY: event.deltaY, - deltaZ: event.deltaZ, - deltaMode: event.deltaMode, - }; - logParent(`Wheel: Sending DispatchWheel dY=${wheelData.deltaY}`); - currentBrowser.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "DispatchWheel", { wheelData }); - }, { capture: true, passive: false }); - - logParent("Edge Scroll Handler Initialized (IPC version)"); -})(); \ No newline at end of file +); \ No newline at end of file From cdb8608f120276480460e109b8c5a44a3327df72 Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Wed, 21 May 2025 00:24:30 +0800 Subject: [PATCH 03/34] Reverted into scroll --- src/zen/common/ZenEdgeScrollFrame.js | 114 ++++++++-------- src/zen/common/ZenEdgeScrollHandler.js | 175 ++++++++++--------------- 2 files changed, 121 insertions(+), 168 deletions(-) diff --git a/src/zen/common/ZenEdgeScrollFrame.js b/src/zen/common/ZenEdgeScrollFrame.js index 4917a112fc..c17cdf1fc5 100644 --- a/src/zen/common/ZenEdgeScrollFrame.js +++ b/src/zen/common/ZenEdgeScrollFrame.js @@ -1,90 +1,82 @@ /* eslint-env mozilla/frame-script */ function logFrame(message) { - dump("ZenEdgeScrollFrame: " + message + "\n"); + console.log("ZenEdgeScrollFrame: " + message + "\n"); } logFrame("Frame script loaded for: " + (content && content.document ? content.document.location.href : "unknown content location")); -addMessageListener("ZenEdgeScroll:SynthesizeMouseEvent", function(message) { - const data = message.data; - if (!data || !data.type) { - logFrame("SynthesizeMouseEvent: Invalid data received."); - return; - } +addMessageListener("ZenEdgeScroll:ScrollToPercentage", function(message) { + const doc = content.document; + // Prefer scrollingElement for consistency, fallback to documentElement or body + const scrollableElement = doc.scrollingElement || doc.documentElement || doc.body; - // clientX, clientY are relative to the content viewport - const targetElement = content.document.elementFromPoint(data.clientX, data.clientY) || content.document.documentElement; + if (scrollableElement && scrollableElement.scrollHeight > scrollableElement.clientHeight) { + const percentage = message.data.percentage; + if (typeof percentage !== 'number') { + logFrame("ScrollToPercentage: Invalid percentage received - " + percentage); + return; + } + + // Store original scroll-behavior and set to auto for instant scroll + const originalScrollBehavior = scrollableElement.style.scrollBehavior; + scrollableElement.style.scrollBehavior = 'auto'; - if (targetElement) { try { - const syntheticEvent = new content.MouseEvent(data.type, { - bubbles: true, - cancelable: (data.type !== 'mousemove'), // mousemove is often not cancelable by default - composed: true, - view: content, - detail: (data.type === 'mousedown' || data.type === 'mouseup' || data.type === 'click') ? 1 : 0, // click count - screenX: data.screenX, - screenY: data.screenY, - clientX: data.clientX, - clientY: data.clientY, - ctrlKey: data.ctrlKey, - altKey: data.altKey, - shiftKey: data.shiftKey, - metaKey: data.metaKey, - button: data.button, - buttons: data.buttons, + const targetScrollTop = percentage * (scrollableElement.scrollHeight - scrollableElement.clientHeight); + scrollableElement.scrollTop = Math.max(0, Math.min(targetScrollTop, scrollableElement.scrollHeight - scrollableElement.clientHeight)); + logFrame(`Scrolled (instant) to ${percentage * 100}%`); + } finally { + // Restore original scroll-behavior + // Using requestAnimationFrame can help ensure the style is restored after the scroll has visually processed. + content.requestAnimationFrame(() => { + scrollableElement.style.scrollBehavior = originalScrollBehavior; }); - targetElement.dispatchEvent(syntheticEvent); - // logFrame(`Dispatched synthetic ${data.type} at (${data.clientX}, ${data.clientY}) on ${targetElement.tagName}`); - } catch (e) { - logFrame(`Error dispatching synthetic ${data.type}: ${e} - ${e.stack}`); } } else { - logFrame(`SynthesizeMouseEvent: No target element found at (${data.clientX}, ${data.clientY})`); + // logFrame("ScrollToPercentage: Content not scrollable or no scrollable element."); } }); -addMessageListener("ZenEdgeScroll:SynthesizeWheelEvent", function(message) { - const data = message.data; - if (!data) { - logFrame("SynthesizeWheelEvent: Invalid data received."); - return; - } - - const targetElement = content.document.elementFromPoint(data.clientX, data.clientY) || content.document.documentElement; +addMessageListener("ZenEdgeScroll:DispatchWheel", function(message) { + const doc = content.document; + // Dispatch to documentElement, as it's a common target and will bubble. + // Or, could try to find the focused element or element under mouse if more precision is needed. + const targetElement = doc.documentElement; // Or doc.body, or content.document.scrollingElement if (targetElement) { + const eventData = message.data.wheelData; + if (!eventData) { + logFrame("DispatchWheel: No eventData received."); + return; + } try { - const syntheticEvent = new content.WheelEvent('wheel', { // type is always 'wheel' + // Use content.WheelEvent to ensure it's the content's native event type + const clonedWheelEvent = new content.WheelEvent("wheel", { + deltaX: eventData.deltaX || 0, + deltaY: eventData.deltaY || 0, + deltaZ: eventData.deltaZ || 0, + deltaMode: eventData.deltaMode || 0, bubbles: true, cancelable: true, composed: true, - view: content, - screenX: data.screenX, - screenY: data.screenY, - clientX: data.clientX, - clientY: data.clientY, - ctrlKey: data.ctrlKey, - altKey: data.altKey, - shiftKey: data.shiftKey, - metaKey: data.metaKey, - button: data.button, // Usually 0 for wheel - buttons: data.buttons, - deltaX: data.deltaX, - deltaY: data.deltaY, - deltaZ: data.deltaZ, - deltaMode: data.deltaMode, + view: content, // 'content' is the window in a frame script + // Pass modifier keys if needed by the page's wheel handlers + ctrlKey: eventData.ctrlKey, + altKey: eventData.altKey, + shiftKey: eventData.shiftKey, + metaKey: eventData.metaKey, }); - targetElement.dispatchEvent(syntheticEvent); - // logFrame(`Dispatched synthetic wheel at (${data.clientX}, ${data.clientY}) on ${targetElement.tagName}`); + targetElement.dispatchEvent(clonedWheelEvent); + // logFrame(`Dispatched wheel event: dY=${eventData.deltaY}`); } catch (e) { - logFrame(`Error dispatching synthetic wheel event: ${e} - ${e.stack}`); + logFrame(`Error dispatching wheel event: ${e} - ${e.stack}`); } } else { - logFrame(`SynthesizeWheelEvent: No target element found at (${data.clientX}, ${data.clientY})`); + // logFrame("DispatchWheel: No targetElement found."); } }); -// Remove the old ScrollToPercentage listener if it's no longer needed -// addMessageListener("ZenEdgeScroll:ScrollToPercentage", function(message) { /* ... */ }); \ No newline at end of file +// Ensure listeners for SynthesizeMouseEvent and SynthesizeWheelEvent are removed or commented out +// addMessageListener("ZenEdgeScroll:SynthesizeMouseEvent", function(message) { /* ... */ }); +// addMessageListener("ZenEdgeScroll:SynthesizeWheelEvent", function(message) { /* ... */ }); \ No newline at end of file diff --git a/src/zen/common/ZenEdgeScrollHandler.js b/src/zen/common/ZenEdgeScrollHandler.js index a0f16434c0..cb5885ba9e 100644 --- a/src/zen/common/ZenEdgeScrollHandler.js +++ b/src/zen/common/ZenEdgeScrollHandler.js @@ -2,6 +2,7 @@ // (Or any other suitable path within your project for browser chrome scripts) (function() { + console.log("hahahahah"); if (window.gEdgeScrollHandlerInitialized) { console.log("EdgeScrollHandler: Already initialized."); return; @@ -49,103 +50,45 @@ logParent(`loadFrameScriptForBrowser: browser.messageManager is null for ${browser.currentURI?.spec}`); } } - - // Initialize frame scripts for existing and new tabs function initFrameScripts() { if (!gBrowser || typeof gBrowser.tabs === 'undefined' || !gBrowser.tabs.length) { logParent("gBrowser not ready or no tabs for initFrameScripts."); - if (!window.gEdgeScrollInitRetryCount || window.gEdgeScrollInitRetryCount < 5) { // Limit retries + if (!window.gEdgeScrollInitRetryCount || window.gEdgeScrollInitRetryCount < 5) { window.gEdgeScrollInitRetryCount = (window.gEdgeScrollInitRetryCount || 0) + 1; setTimeout(initFrameScripts, 500 * window.gEdgeScrollInitRetryCount); } return; } - logParent("Initializing frame scripts for tabs..."); for (const tab of gBrowser.tabs) { - if (tab.linkedBrowser) { - loadFrameScriptForBrowser(tab.linkedBrowser); - } + if (tab.linkedBrowser) loadFrameScriptForBrowser(tab.linkedBrowser); } - gBrowser.tabContainer.addEventListener("TabOpen", event => { - const browser = event.target.linkedBrowser; - if (browser) { - // Frame scripts are often loaded on demand or when the browser is ready - // Listening for 'load' or 'DOMContentLoaded' on the browser might be more robust - // For now, direct load on TabOpen - loadFrameScriptForBrowser(browser); - } + if (event.target.linkedBrowser) loadFrameScriptForBrowser(event.target.linkedBrowser); }); - gBrowser.tabContainer.addEventListener("TabSelect", event => { - const browser = event.target.linkedBrowser; - if (browser) { - loadFrameScriptForBrowser(browser); - } + if (event.target.linkedBrowser) loadFrameScriptForBrowser(event.target.linkedBrowser); }); - - if (gBrowser.selectedBrowser) { - loadFrameScriptForBrowser(gBrowser.selectedBrowser); - } + if (gBrowser.selectedBrowser) loadFrameScriptForBrowser(gBrowser.selectedBrowser); } - function getGapZoneInfo(event) { const activeBrowser = gBrowser.selectedBrowser; - if (!activeBrowser) { - return { isInGap: false, targetBrowser: null, browserRect: null }; - } + if (!activeBrowser) return { isInGap: false, targetBrowser: null, browserRect: null }; const activeBrowserRect = activeBrowser.getBoundingClientRect(); const windowWidth = mainBrowserWindow.innerWidth; - const isActiveBrowserRightmost = - (windowWidth - activeBrowserRect.right) < RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX; - - if (!isActiveBrowserRightmost) { - return { isInGap: false, targetBrowser: null, browserRect: null }; - } - - const isInFarRightWindowEdgeGap = event.clientX > (windowWidth - EDGE_INTERACTION_WIDTH_PX) && - event.clientX < windowWidth; + const isActiveBrowserRightmost = (windowWidth - activeBrowserRect.right) < RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX; + if (!isActiveBrowserRightmost) return { isInGap: false, targetBrowser: null, browserRect: null }; + const isInFarRightWindowEdgeGap = event.clientX > (windowWidth - EDGE_INTERACTION_WIDTH_PX) && event.clientX < windowWidth; if (isInFarRightWindowEdgeGap) { - return { - isInGap: true, - targetBrowser: activeBrowser, - browserRect: activeBrowserRect, - }; + return { isInGap: true, targetBrowser: activeBrowser, browserRect: activeBrowserRect }; } return { isInGap: false, targetBrowser: null, browserRect: null }; } - function createSyntheticEventData(originalEvent, targetBrowserRect, eventType) { - const clientXInContent = Math.max(0, Math.floor(targetBrowserRect.width - SYNTHETIC_EVENT_OFFSET_FROM_EDGE)); - const clientYInContent = Math.max(0, Math.min(Math.floor(originalEvent.clientY - targetBrowserRect.top), Math.floor(targetBrowserRect.height - 1))); - - const screenX = Math.floor(mainBrowserWindow.screenX + targetBrowserRect.left + clientXInContent); - const screenY = Math.floor(mainBrowserWindow.screenY + targetBrowserRect.top + clientYInContent); - - return { - type: eventType, - clientX: clientXInContent, - clientY: clientYInContent, - screenX: screenX, - screenY: screenY, - button: originalEvent.button, - buttons: originalEvent.buttons, - ctrlKey: originalEvent.ctrlKey, - altKey: originalEvent.altKey, - shiftKey: originalEvent.shiftKey, - metaKey: originalEvent.metaKey, - deltaX: originalEvent.deltaX || 0, // Ensure these exist for wheel - deltaY: originalEvent.deltaY || 0, - deltaZ: originalEvent.deltaZ || 0, - deltaMode: originalEvent.deltaMode || 0, - }; - } - mainBrowserWindow.addEventListener('mousedown', (event) => { if (event.button !== 0) return; @@ -160,58 +103,68 @@ loadFrameScriptForBrowser(targetBrowser); event.preventDefault(); - isSynthesizingDrag = true; + isDraggingEdgeScroll = true; // Changed back + dragInitialModel.targetBrowserDuringDrag = targetBrowser; + dragInitialModel.gapZoneHeight = gapInfo.browserRect.height; // Height of the content area + dragInitialModel.mouseY = event.clientY; // Initial mouse Y for drag calculation + + // Calculate scroll percentage based on click position within the "virtual scrollbar" (gap area) + const clickYInGap = event.clientY - gapInfo.browserRect.top; + const scrollPercentage = dragInitialModel.gapZoneHeight > 0 ? Math.max(0, Math.min(1, clickYInGap / dragInitialModel.gapZoneHeight)) : 0; + dragInitialModel.initialScrollPercentageOnDragStart = scrollPercentage; - const eventData = createSyntheticEventData(event, gapInfo.browserRect, 'mousedown'); - targetBrowser.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "SynthesizeMouseEvent", eventData); + targetBrowser.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "ScrollToPercentage", { percentage: scrollPercentage }); - mainBrowserWindow.addEventListener('mousemove', handleSyntheticDrag, true); // Use capture for mousemove - mainBrowserWindow.addEventListener('mouseup', handleSyntheticDragEnd, true); // Use capture for mouseup - }, true); // Use capture for mousedown + mainBrowserWindow.addEventListener('mousemove', handleEdgeScrollDrag, true); + mainBrowserWindow.addEventListener('mouseup', handleEdgeScrollEnd, true); + }, true); - function handleSyntheticDrag(event) { - if (!isSynthesizingDrag || !dragInitialModel.targetBrowserDuringDrag) return; + function handleEdgeScrollDrag(event) { // Renamed back + if (!isDraggingEdgeScroll || !dragInitialModel.targetBrowserDuringDrag) return; const targetBrowser = dragInitialModel.targetBrowserDuringDrag; - if (gBrowser.selectedBrowser !== targetBrowser || !targetBrowser.messageManager || !targetBrowser.contentWindow) { + if (gBrowser.selectedBrowser !== targetBrowser || !targetBrowser.messageManager) { logParent("Drag: Target browser changed, lost messageManager, or no contentWindow. Ending drag."); - handleSyntheticDragEnd(event); + handleEdgeScrollEnd(event); return; } event.preventDefault(); - event.stopPropagation(); // Stop event from bubbling further in parent + event.stopPropagation(); - const targetBrowserRect = targetBrowser.getBoundingClientRect(); - const eventData = createSyntheticEventData(event, targetBrowserRect, 'mousemove'); - targetBrowser.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "SynthesizeMouseEvent", eventData); + const deltaYFromInitialMouseY = event.clientY - dragInitialModel.mouseY; + let newScrollPercentage; + + if (dragInitialModel.gapZoneHeight > 0) { + // Calculate how much the mouse has moved as a percentage of the gap height + const percentageDelta = deltaYFromInitialMouseY / dragInitialModel.gapZoneHeight; + // Add this delta to the scroll percentage we had when the drag started + newScrollPercentage = dragInitialModel.initialScrollPercentageOnDragStart + percentageDelta; + newScrollPercentage = Math.max(0, Math.min(1, newScrollPercentage)); // Clamp between 0 and 1 + } else { + // Fallback or error - should not happen if drag started correctly + return; + } + + targetBrowser.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "ScrollToPercentage", { percentage: newScrollPercentage }); } - function handleSyntheticDragEnd(event) { - if (isSynthesizingDrag && dragInitialModel.targetBrowserDuringDrag) { - const targetBrowser = dragInitialModel.targetBrowserDuringDrag; - if (targetBrowser.messageManager) { - // Ensure event is not null if called without one (e.g. from a cleanup path) - if (event) { - event.preventDefault(); - event.stopPropagation(); - const targetBrowserRect = targetBrowser.getBoundingClientRect(); - const eventData = createSyntheticEventData(event, targetBrowserRect, 'mouseup'); - targetBrowser.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "SynthesizeMouseEvent", eventData); - } else { - // If event is null, we might not have coordinates for mouseup. - // Depending on desired behavior, could send a generic mouseup or skip. - // For now, we only send if we have an event. - logParent("DragEnd: Called without an event, mouseup not synthesized to content."); - } + function handleEdgeScrollEnd(event) { // Renamed back + if (isDraggingEdgeScroll && dragInitialModel.targetBrowserDuringDrag) { + if (event) { // If called by an event + event.preventDefault(); + event.stopPropagation(); + // Optionally, could send a final ScrollToPercentage if needed, but current logic updates on mousemove } } - isSynthesizingDrag = false; + isDraggingEdgeScroll = false; + // Reset parts of dragInitialModel if they are large or sensitive dragInitialModel.targetBrowserDuringDrag = null; - mainBrowserWindow.removeEventListener('mousemove', handleSyntheticDrag, true); - mainBrowserWindow.removeEventListener('mouseup', handleSyntheticDragEnd, true); - // logParent("DragEnd: Event listeners removed."); + // dragInitialModel.mouseY = 0; // etc. + + mainBrowserWindow.removeEventListener('mousemove', handleEdgeScrollDrag, true); + mainBrowserWindow.removeEventListener('mouseup', handleEdgeScrollEnd, true); } mainBrowserWindow.addEventListener('wheel', (event) => { @@ -228,12 +181,20 @@ event.preventDefault(); event.stopPropagation(); - const eventData = createSyntheticEventData(event, gapInfo.browserRect, 'wheel'); // type 'wheel' is just for our data - targetBrowser.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "SynthesizeWheelEvent", eventData); + const wheelData = { + deltaX: event.deltaX, + deltaY: event.deltaY, + deltaZ: event.deltaZ, + deltaMode: event.deltaMode, + // Pass necessary modifier keys if the frame script needs them for dispatching + ctrlKey: event.ctrlKey, + altKey: event.altKey, + shiftKey: event.shiftKey, + metaKey: event.metaKey, + }; + targetBrowser.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "DispatchWheel", { wheelData }); }, { capture: true, passive: false }); - // Initialize frame script loading logic - // Ensure gBrowser is available before initializing if (document.readyState === "complete" || document.readyState === "interactive") { initFrameScripts(); } else { From f93211745c0bcca246e1afa2ade121bec3f3d248 Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Wed, 21 May 2025 00:24:30 +0800 Subject: [PATCH 04/34] Activate adjacent browser --- src/zen/common/ZenEdgeScrollHandler.js | 169 +++++++++++++++++++------ 1 file changed, 127 insertions(+), 42 deletions(-) diff --git a/src/zen/common/ZenEdgeScrollHandler.js b/src/zen/common/ZenEdgeScrollHandler.js index cb5885ba9e..7b71786726 100644 --- a/src/zen/common/ZenEdgeScrollHandler.js +++ b/src/zen/common/ZenEdgeScrollHandler.js @@ -72,61 +72,141 @@ if (gBrowser.selectedBrowser) loadFrameScriptForBrowser(gBrowser.selectedBrowser); } + /** + * Identifies if the mouse event is in the "gap" and which browser pane is adjacent. + */ function getGapZoneInfo(event) { - const activeBrowser = gBrowser.selectedBrowser; - if (!activeBrowser) return { isInGap: false, targetBrowser: null, browserRect: null }; - - const activeBrowserRect = activeBrowser.getBoundingClientRect(); const windowWidth = mainBrowserWindow.innerWidth; + const eventClientX = event.clientX; + const eventClientY = event.clientY; - const isActiveBrowserRightmost = (windowWidth - activeBrowserRect.right) < RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX; - if (!isActiveBrowserRightmost) return { isInGap: false, targetBrowser: null, browserRect: null }; + const isInFarRightWindowEdgeGap = eventClientX > (windowWidth - EDGE_INTERACTION_WIDTH_PX) && + eventClientX < windowWidth; - const isInFarRightWindowEdgeGap = event.clientX > (windowWidth - EDGE_INTERACTION_WIDTH_PX) && event.clientX < windowWidth; - if (isInFarRightWindowEdgeGap) { - return { isInGap: true, targetBrowser: activeBrowser, browserRect: activeBrowserRect }; + if (!isInFarRightWindowEdgeGap) { + return { isInGap: false, targetBrowser: null, browserRect: null }; + } + + // Find the browser pane adjacent to this gap click + let potentialTargetBrowser = null; + let potentialTargetBrowserRect = null; + + if (gBrowser && gBrowser.browsers) { + for (const browser of gBrowser.browsers) { + // Ensure browser is visible and has a content window + if (browser.hidden) continue; + + const browserRect = browser.getBoundingClientRect(); + if (browserRect.width === 0 || browserRect.height === 0) continue; // Skip non-rendered browsers + + // Check if this browser is at the far right edge of the window + const isBrowserAtRightEdge = (windowWidth - browserRect.right) < RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX && + browserRect.right > (windowWidth - EDGE_INTERACTION_WIDTH_PX - RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX); + + const isEventYWithinBrowser = eventClientY >= browserRect.top && eventClientY <= browserRect.bottom; + + if (isBrowserAtRightEdge && isEventYWithinBrowser) { + potentialTargetBrowser = browser; + potentialTargetBrowserRect = browserRect; + // logParent(`GapZoneInfo: Potential adjacent browser: ${browser.currentURI?.spec} at right edge. Rect: R=${browserRect.right}, T=${browserRect.top}, B=${browserRect.bottom}`); + break; + } + } + } + + if (potentialTargetBrowser) { + // logParent(`GapZoneInfo: Found adjacent browser: ${potentialTargetBrowser.currentURI?.spec}`); + return { + isInGap: true, + targetBrowser: potentialTargetBrowser, + browserRect: potentialTargetBrowserRect, + }; } - return { isInGap: false, targetBrowser: null, browserRect: null }; + + // logParent("GapZoneInfo: Mouse in gap, but no adjacent browser found at that Y position."); + // Return isInGap true because the click was in the edge zone, even if no specific browser was pinpointed. + // The mousedown handler will then decide if it can proceed without a targetBrowser. + return { isInGap: true, targetBrowser: null, browserRect: null }; } mainBrowserWindow.addEventListener('mousedown', (event) => { if (event.button !== 0) return; const gapInfo = getGapZoneInfo(event); - if (!gapInfo.isInGap || !gapInfo.targetBrowser) return; - const targetBrowser = gapInfo.targetBrowser; - if (!targetBrowser.messageManager) { - logParent("Mousedown: No messageManager for target browser."); + if (!gapInfo.isInGap || !gapInfo.targetBrowser) { + if (gapInfo.isInGap) { + logParent("Mousedown: In gap, but no specific adjacent browser identified by getGapZoneInfo."); + } + return; + } + + let targetBrowser = gapInfo.targetBrowser; // This is the browser physically adjacent to the gap + + // Focus/select the target browser if it's not already the selected one + if (targetBrowser !== gBrowser.selectedBrowser) { + logParent(`Mousedown: Adjacent browser ${targetBrowser.currentURI?.spec} is not selected. Attempting to select.`); + // Ensure we are interacting with the gBrowser instance of the main window + const mainGBrowser = mainBrowserWindow.gBrowser; + if (mainGBrowser && targetBrowser.ownerGlobal && targetBrowser.ownerGlobal.gBrowser === mainGBrowser) { + const tabToSelect = mainGBrowser.getTabForBrowser(targetBrowser); + if (tabToSelect && mainGBrowser.selectedTab !== tabToSelect) { + mainGBrowser.selectedTab = tabToSelect; + logParent(`Mousedown: Switched selectedTab to the one for ${targetBrowser.currentURI?.spec}.`); + // After tab switch, gBrowser.selectedBrowser should update. + // Re-assign targetBrowser to be sure it's the currently selected one. + targetBrowser = mainGBrowser.selectedBrowser; + // Update browserRect as it might change after selection (e.g., if UI elements shift) + // This is important for accurate gapZoneHeight calculation. + if (targetBrowser) { // Check if targetBrowser is still valid after selection + gapInfo.browserRect = targetBrowser.getBoundingClientRect(); + } else { + logParent("Mousedown: Target browser became null after attempting selection. Aborting."); + return; + } + } else if (tabToSelect && mainGBrowser.selectedTab === tabToSelect) { + logParent(`Mousedown: Adjacent browser ${targetBrowser.currentURI?.spec} was already selected.`); + } else { + logParent(`Mousedown: Could not find tab for adjacent browser ${targetBrowser.currentURI?.spec}.`); + } + } else { + logParent(`Mousedown: Cannot determine tab for adjacent browser ${targetBrowser.currentURI?.spec}.`); + } + } + + if (!targetBrowser || !targetBrowser.messageManager) { + logParent("Mousedown: No messageManager for target browser (either original or after selection)."); return; } loadFrameScriptForBrowser(targetBrowser); event.preventDefault(); - isDraggingEdgeScroll = true; // Changed back + isDraggingEdgeScroll = true; dragInitialModel.targetBrowserDuringDrag = targetBrowser; - dragInitialModel.gapZoneHeight = gapInfo.browserRect.height; // Height of the content area - dragInitialModel.mouseY = event.clientY; // Initial mouse Y for drag calculation + dragInitialModel.gapZoneHeight = gapInfo.browserRect.height; + dragInitialModel.mouseY = event.clientY; - // Calculate scroll percentage based on click position within the "virtual scrollbar" (gap area) const clickYInGap = event.clientY - gapInfo.browserRect.top; const scrollPercentage = dragInitialModel.gapZoneHeight > 0 ? Math.max(0, Math.min(1, clickYInGap / dragInitialModel.gapZoneHeight)) : 0; dragInitialModel.initialScrollPercentageOnDragStart = scrollPercentage; + logParent(`Mousedown on ${targetBrowser.currentURI?.spec}: gapHeight=${dragInitialModel.gapZoneHeight.toFixed(2)}, clickYInGap=${clickYInGap.toFixed(2)}, initialMouseY=${dragInitialModel.mouseY.toFixed(2)}, initialScrollPerc=${scrollPercentage.toFixed(4)}`); + targetBrowser.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "ScrollToPercentage", { percentage: scrollPercentage }); mainBrowserWindow.addEventListener('mousemove', handleEdgeScrollDrag, true); mainBrowserWindow.addEventListener('mouseup', handleEdgeScrollEnd, true); }, true); - function handleEdgeScrollDrag(event) { // Renamed back + function handleEdgeScrollDrag(event) { if (!isDraggingEdgeScroll || !dragInitialModel.targetBrowserDuringDrag) return; const targetBrowser = dragInitialModel.targetBrowserDuringDrag; + // Ensure the browser being dragged is still the selected one, and it's valid if (gBrowser.selectedBrowser !== targetBrowser || !targetBrowser.messageManager) { logParent("Drag: Target browser changed, lost messageManager, or no contentWindow. Ending drag."); - handleEdgeScrollEnd(event); + handleEdgeScrollEnd(event); // Pass event for proper cleanup if needed return; } event.preventDefault(); @@ -136,42 +216,54 @@ let newScrollPercentage; if (dragInitialModel.gapZoneHeight > 0) { - // Calculate how much the mouse has moved as a percentage of the gap height const percentageDelta = deltaYFromInitialMouseY / dragInitialModel.gapZoneHeight; - // Add this delta to the scroll percentage we had when the drag started newScrollPercentage = dragInitialModel.initialScrollPercentageOnDragStart + percentageDelta; - newScrollPercentage = Math.max(0, Math.min(1, newScrollPercentage)); // Clamp between 0 and 1 + newScrollPercentage = Math.max(0, Math.min(1, newScrollPercentage)); + // logParent(`Drag: clientY=${event.clientY.toFixed(2)}, deltaMouseY=${deltaYFromInitialMouseY.toFixed(2)}, percDelta=${percentageDelta.toFixed(4)}, newScrollPerc=${newScrollPercentage.toFixed(4)}`); } else { - // Fallback or error - should not happen if drag started correctly + logParent("Drag: Error - gapZoneHeight is 0 or invalid. Cannot calculate scroll percentage."); + handleEdgeScrollEnd(event); // End drag if calculation is impossible return; } targetBrowser.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "ScrollToPercentage", { percentage: newScrollPercentage }); } - function handleEdgeScrollEnd(event) { // Renamed back + function handleEdgeScrollEnd(event) { if (isDraggingEdgeScroll && dragInitialModel.targetBrowserDuringDrag) { - if (event) { // If called by an event + if (event) { event.preventDefault(); event.stopPropagation(); - // Optionally, could send a final ScrollToPercentage if needed, but current logic updates on mousemove } + // logParent("DragEnd: Releasing drag."); } isDraggingEdgeScroll = false; - // Reset parts of dragInitialModel if they are large or sensitive dragInitialModel.targetBrowserDuringDrag = null; - // dragInitialModel.mouseY = 0; // etc. mainBrowserWindow.removeEventListener('mousemove', handleEdgeScrollDrag, true); mainBrowserWindow.removeEventListener('mouseup', handleEdgeScrollEnd, true); } mainBrowserWindow.addEventListener('wheel', (event) => { - const gapInfo = getGapZoneInfo(event); - if (!gapInfo.isInGap || !gapInfo.targetBrowser) return; - - const targetBrowser = gapInfo.targetBrowser; + // For wheel events, we can use a similar logic to find the target browser + // or decide if wheel events in the gap should always target the *currently selected* rightmost browser. + // For now, let's keep the wheel targeting logic simpler: it targets the *selected* browser if it's rightmost. + // If you want wheel to also switch focus, getGapZoneInfo would need to be called here too, + // and focus switching logic similar to mousedown would be needed. + + const activeBrowser = gBrowser.selectedBrowser; + if (!activeBrowser) return; + + const windowWidth = mainBrowserWindow.innerWidth; + const activeBrowserRect = activeBrowser.getBoundingClientRect(); + const isActiveBrowserRightmost = (windowWidth - activeBrowserRect.right) < RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX; + const isInFarRightWindowEdgeGap = event.clientX > (windowWidth - EDGE_INTERACTION_WIDTH_PX) && event.clientX < windowWidth; + + if (!isActiveBrowserRightmost || !isInFarRightWindowEdgeGap) return; + + const targetBrowser = activeBrowser; // Wheel targets the active browser if it's rightmost and mouse is in gap + if (!targetBrowser.messageManager) { logParent("Wheel: No messageManager for target browser."); return; @@ -182,15 +274,8 @@ event.stopPropagation(); const wheelData = { - deltaX: event.deltaX, - deltaY: event.deltaY, - deltaZ: event.deltaZ, - deltaMode: event.deltaMode, - // Pass necessary modifier keys if the frame script needs them for dispatching - ctrlKey: event.ctrlKey, - altKey: event.altKey, - shiftKey: event.shiftKey, - metaKey: event.metaKey, + deltaX: event.deltaX, deltaY: event.deltaY, deltaZ: event.deltaZ, deltaMode: event.deltaMode, + ctrlKey: event.ctrlKey, altKey: event.altKey, shiftKey: event.shiftKey, metaKey: event.metaKey, }; targetBrowser.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "DispatchWheel", { wheelData }); }, { capture: true, passive: false }); From 73bde70232e4734ca2405d70ed191e64ef099fd9 Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Wed, 21 May 2025 00:24:30 +0800 Subject: [PATCH 05/34] Try to synthesize mouse event again --- src/zen/common/ZenEdgeScrollFrame.js | 118 ++++++----- src/zen/common/ZenEdgeScrollHandler.js | 266 ++++++++++--------------- 2 files changed, 164 insertions(+), 220 deletions(-) diff --git a/src/zen/common/ZenEdgeScrollFrame.js b/src/zen/common/ZenEdgeScrollFrame.js index c17cdf1fc5..6b80608610 100644 --- a/src/zen/common/ZenEdgeScrollFrame.js +++ b/src/zen/common/ZenEdgeScrollFrame.js @@ -1,82 +1,92 @@ /* eslint-env mozilla/frame-script */ function logFrame(message) { + // Use dump for more reliable output from frame scripts to the system console console.log("ZenEdgeScrollFrame: " + message + "\n"); + // console.log("ZenEdgeScrollFrame: " + message + "\n"); // Can also be used, output goes to Browser Console } logFrame("Frame script loaded for: " + (content && content.document ? content.document.location.href : "unknown content location")); +// REMOVE or COMMENT OUT the old ScrollToPercentage listener +/* addMessageListener("ZenEdgeScroll:ScrollToPercentage", function(message) { - const doc = content.document; - // Prefer scrollingElement for consistency, fallback to documentElement or body - const scrollableElement = doc.scrollingElement || doc.documentElement || doc.body; + // ... old code ... +}); +*/ - if (scrollableElement && scrollableElement.scrollHeight > scrollableElement.clientHeight) { - const percentage = message.data.percentage; - if (typeof percentage !== 'number') { - logFrame("ScrollToPercentage: Invalid percentage received - " + percentage); - return; - } +addMessageListener("ZenEdgeScroll:SynthesizeMouseEvent", function(message) { + const data = message.data; + if (!data || !data.type) { + logFrame("SynthesizeMouseEvent: Invalid data received."); + return; + } - // Store original scroll-behavior and set to auto for instant scroll - const originalScrollBehavior = scrollableElement.style.scrollBehavior; - scrollableElement.style.scrollBehavior = 'auto'; + // clientX, clientY are relative to the content viewport, calculated by parent + // For scrollbar interaction, dispatching on documentElement or scrollingElement is usually effective. + const targetElement = content.document.elementFromPoint(data.clientX, data.clientY) || content.document.documentElement || content.document.body; + + if (!targetElement) { + logFrame(`SynthesizeMouseEvent: No target element found at (${data.clientX}, ${data.clientY}) for ${data.type}`); + return; + } + // logFrame(`SynthesizeMouseEvent: Dispatching ${data.type} at X:${data.clientX}, Y:${data.clientY} on ${targetElement.tagName}`); - try { - const targetScrollTop = percentage * (scrollableElement.scrollHeight - scrollableElement.clientHeight); - scrollableElement.scrollTop = Math.max(0, Math.min(targetScrollTop, scrollableElement.scrollHeight - scrollableElement.clientHeight)); - logFrame(`Scrolled (instant) to ${percentage * 100}%`); - } finally { - // Restore original scroll-behavior - // Using requestAnimationFrame can help ensure the style is restored after the scroll has visually processed. - content.requestAnimationFrame(() => { - scrollableElement.style.scrollBehavior = originalScrollBehavior; - }); - } - } else { - // logFrame("ScrollToPercentage: Content not scrollable or no scrollable element."); + try { + const syntheticEvent = new content.MouseEvent(data.type, { + bubbles: true, + cancelable: (data.type !== 'mousemove'), // mousemove is often not cancelable + composed: true, + view: content, // Essential: the content window + detail: (data.type === 'mousedown' || data.type === 'mouseup' || data.type === 'click') ? 1 : 0, + screenX: data.screenX, + screenY: data.screenY, + clientX: data.clientX, + clientY: data.clientY, + ctrlKey: data.ctrlKey, + altKey: data.altKey, + shiftKey: data.shiftKey, + metaKey: data.metaKey, + button: data.button, + buttons: data.buttons, // Crucial for dragging state + }); + targetElement.dispatchEvent(syntheticEvent); + } catch (e) { + logFrame(`Error dispatching synthetic ${data.type} event: ${e} - ${e.stack}`); } }); + addMessageListener("ZenEdgeScroll:DispatchWheel", function(message) { const doc = content.document; - // Dispatch to documentElement, as it's a common target and will bubble. - // Or, could try to find the focused element or element under mouse if more precision is needed. - const targetElement = doc.documentElement; // Or doc.body, or content.document.scrollingElement + const eventData = message.data.wheelData; + if (!eventData) { + logFrame("DispatchWheel: No eventData received."); + return; + } + + // Use clientX/Y from eventData if provided, otherwise fallback + const clientX = typeof eventData.clientX === 'number' ? eventData.clientX : (doc.documentElement.clientWidth / 2); + const clientY = typeof eventData.clientY === 'number' ? eventData.clientY : (doc.documentElement.clientHeight / 2); + + const targetElement = doc.elementFromPoint(clientX, clientY) || doc.documentElement || doc.body; if (targetElement) { - const eventData = message.data.wheelData; - if (!eventData) { - logFrame("DispatchWheel: No eventData received."); - return; - } + // logFrame(`DispatchWheel: Dispatching on ${targetElement.tagName} at X:${clientX}, Y:${clientY}`); try { - // Use content.WheelEvent to ensure it's the content's native event type const clonedWheelEvent = new content.WheelEvent("wheel", { - deltaX: eventData.deltaX || 0, - deltaY: eventData.deltaY || 0, - deltaZ: eventData.deltaZ || 0, - deltaMode: eventData.deltaMode || 0, - bubbles: true, - cancelable: true, - composed: true, - view: content, // 'content' is the window in a frame script - // Pass modifier keys if needed by the page's wheel handlers - ctrlKey: eventData.ctrlKey, - altKey: eventData.altKey, - shiftKey: eventData.shiftKey, - metaKey: eventData.metaKey, + deltaX: eventData.deltaX || 0, deltaY: eventData.deltaY || 0, deltaZ: eventData.deltaZ || 0, + deltaMode: eventData.deltaMode || 0, bubbles: true, cancelable: true, composed: true, view: content, + ctrlKey: eventData.ctrlKey, altKey: eventData.altKey, shiftKey: eventData.shiftKey, metaKey: eventData.metaKey, + clientX: clientX, clientY: clientY, // Include clientX/Y in the event itself + screenX: eventData.screenX, screenY: eventData.screenY, // If available + button: 0, buttons: 0, // Wheel events typically don't have button presses }); targetElement.dispatchEvent(clonedWheelEvent); - // logFrame(`Dispatched wheel event: dY=${eventData.deltaY}`); } catch (e) { logFrame(`Error dispatching wheel event: ${e} - ${e.stack}`); } } else { - // logFrame("DispatchWheel: No targetElement found."); + logFrame("DispatchWheel: No targetElement found."); } -}); - -// Ensure listeners for SynthesizeMouseEvent and SynthesizeWheelEvent are removed or commented out -// addMessageListener("ZenEdgeScroll:SynthesizeMouseEvent", function(message) { /* ... */ }); -// addMessageListener("ZenEdgeScroll:SynthesizeWheelEvent", function(message) { /* ... */ }); \ No newline at end of file +}); \ No newline at end of file diff --git a/src/zen/common/ZenEdgeScrollHandler.js b/src/zen/common/ZenEdgeScrollHandler.js index 7b71786726..44116aeb4b 100644 --- a/src/zen/common/ZenEdgeScrollHandler.js +++ b/src/zen/common/ZenEdgeScrollHandler.js @@ -1,65 +1,49 @@ -// Suggested filepath: /home/stnav/Projects/others/zen-desktop/src/zen/ui/EdgeScrollHandler.js -// (Or any other suitable path within your project for browser chrome scripts) - (function() { - console.log("hahahahah"); if (window.gEdgeScrollHandlerInitialized) { - console.log("EdgeScrollHandler: Already initialized."); return; } window.gEdgeScrollHandlerInitialized = true; - const FRAME_SCRIPT_URL = "chrome://browser/content/ZenEdgeScrollFrame.js"; // Ensure this matches your frame script's packaged URI + const FRAME_SCRIPT_URL = "chrome://browser/content/ZenEdgeScrollFrame.js"; const MESSAGE_PREFIX = "ZenEdgeScroll:"; - // --- START: Configuration for edge interaction --- - const EDGE_INTERACTION_WIDTH_PX = 20; // How wide the clickable zone at the far right edge of the window is. - const RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX = 25; // Increased slightly for more tolerance with window borders/shadows - const SYNTHETIC_EVENT_OFFSET_FROM_EDGE = 3; // How many pixels inside the content edge to target (e.g., for scrollbar thumb) - // --- END: Configuration for edge interaction --- + const EDGE_INTERACTION_WIDTH_PX = 20; + const RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX = 25; + // How many pixels from the right edge of the content viewport to target the synthetic click + const SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE = 8; // Aim for the scrollbar track - const mainBrowserWindow = window; // 'window' in this context is the main browser window - let isSynthesizingDrag = false; + const mainBrowserWindow = window; + let isSynthesizingDrag = false; // Changed from isDraggingEdgeScroll let dragInitialModel = { targetBrowserDuringDrag: null, - // No scroll-specific properties needed here anymore + // No scroll-percentage specific model needed here, but we store the target browser }; function logParent(message) { // console.log("EdgeScrollHandler (Parent): " + message); - // dump("EdgeScrollHandler (Parent): " + message + "\n"); // For terminal output + dump("EdgeScrollHandler (Parent): " + message + "\n"); } - // Function to load the frame script into a browser's message manager function loadFrameScriptForBrowser(browser) { if (browser && browser.messageManager && !browser.frameScriptLoadedForEdgeScroll) { try { - logParent(`Attempting to load frame script: ${FRAME_SCRIPT_URL} for browser: ${browser.currentURI?.spec}`); - browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false); // false = don't delay - browser.frameScriptLoadedForEdgeScroll = true; // Custom property to track loading - logParent(`Frame script loading initiated for ${browser.currentURI?.spec || 'browser'}.`); + browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false); + browser.frameScriptLoadedForEdgeScroll = true; } catch (e) { - console.error("EdgeScrollHandler (Parent): CRITICAL ERROR loading frame script:", FRAME_SCRIPT_URL, e, "for URL:", browser.currentURI?.spec); + console.error("EdgeScrollHandler (Parent): CRITICAL ERROR loading frame script:", FRAME_SCRIPT_URL, e); dump(`EdgeScrollHandler (Parent): CRITICAL ERROR loading frame script: ${FRAME_SCRIPT_URL} - ${e} - ${e.stack}\n`); } - } else if (browser && browser.frameScriptLoadedForEdgeScroll) { - // logParent(`Frame script already marked as loaded for ${browser.currentURI?.spec}`); - } else if (!browser) { - logParent("loadFrameScriptForBrowser: browser is null"); - } else if (!browser.messageManager) { - logParent(`loadFrameScriptForBrowser: browser.messageManager is null for ${browser.currentURI?.spec}`); } } + function initFrameScripts() { if (!gBrowser || typeof gBrowser.tabs === 'undefined' || !gBrowser.tabs.length) { - logParent("gBrowser not ready or no tabs for initFrameScripts."); if (!window.gEdgeScrollInitRetryCount || window.gEdgeScrollInitRetryCount < 5) { window.gEdgeScrollInitRetryCount = (window.gEdgeScrollInitRetryCount || 0) + 1; setTimeout(initFrameScripts, 500 * window.gEdgeScrollInitRetryCount); } return; } - logParent("Initializing frame scripts for tabs..."); for (const tab of gBrowser.tabs) { if (tab.linkedBrowser) loadFrameScriptForBrowser(tab.linkedBrowser); } @@ -72,211 +56,162 @@ if (gBrowser.selectedBrowser) loadFrameScriptForBrowser(gBrowser.selectedBrowser); } - /** - * Identifies if the mouse event is in the "gap" and which browser pane is adjacent. - */ function getGapZoneInfo(event) { const windowWidth = mainBrowserWindow.innerWidth; const eventClientX = event.clientX; const eventClientY = event.clientY; + const isInFarRightWindowEdgeGap = eventClientX > (windowWidth - EDGE_INTERACTION_WIDTH_PX) && eventClientX < windowWidth; + if (!isInFarRightWindowEdgeGap) return { isInGap: false, targetBrowser: null, browserRect: null }; - const isInFarRightWindowEdgeGap = eventClientX > (windowWidth - EDGE_INTERACTION_WIDTH_PX) && - eventClientX < windowWidth; - - if (!isInFarRightWindowEdgeGap) { - return { isInGap: false, targetBrowser: null, browserRect: null }; - } - - // Find the browser pane adjacent to this gap click let potentialTargetBrowser = null; let potentialTargetBrowserRect = null; - if (gBrowser && gBrowser.browsers) { for (const browser of gBrowser.browsers) { - // Ensure browser is visible and has a content window if (browser.hidden) continue; - const browserRect = browser.getBoundingClientRect(); - if (browserRect.width === 0 || browserRect.height === 0) continue; // Skip non-rendered browsers - - // Check if this browser is at the far right edge of the window - const isBrowserAtRightEdge = (windowWidth - browserRect.right) < RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX && - browserRect.right > (windowWidth - EDGE_INTERACTION_WIDTH_PX - RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX); - + if (browserRect.width === 0 || browserRect.height === 0) continue; + const isBrowserAtRightEdge = (windowWidth - browserRect.right) < RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX && browserRect.right > (windowWidth - EDGE_INTERACTION_WIDTH_PX - RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX); const isEventYWithinBrowser = eventClientY >= browserRect.top && eventClientY <= browserRect.bottom; - if (isBrowserAtRightEdge && isEventYWithinBrowser) { potentialTargetBrowser = browser; potentialTargetBrowserRect = browserRect; - // logParent(`GapZoneInfo: Potential adjacent browser: ${browser.currentURI?.spec} at right edge. Rect: R=${browserRect.right}, T=${browserRect.top}, B=${browserRect.bottom}`); - break; + break; } } } - if (potentialTargetBrowser) { - // logParent(`GapZoneInfo: Found adjacent browser: ${potentialTargetBrowser.currentURI?.spec}`); - return { - isInGap: true, - targetBrowser: potentialTargetBrowser, - browserRect: potentialTargetBrowserRect, - }; + return { isInGap: true, targetBrowser: potentialTargetBrowser, browserRect: potentialTargetBrowserRect }; } - - // logParent("GapZoneInfo: Mouse in gap, but no adjacent browser found at that Y position."); - // Return isInGap true because the click was in the edge zone, even if no specific browser was pinpointed. - // The mousedown handler will then decide if it can proceed without a targetBrowser. return { isInGap: true, targetBrowser: null, browserRect: null }; } + // Helper to create common event data for IPC + function createSyntheticEventData(originalEvent, targetBrowserRect, eventType) { + // Coordinates relative to the target browser's viewport + // Horizontal position: fixed, near the right edge (scrollbar) + const clientXInContent = Math.max(0, Math.floor(targetBrowserRect.width - SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE)); + // Vertical position: mirrors the original event's Y relative to the browser's top + const clientYInContent = Math.max(0, Math.min(Math.floor(originalEvent.clientY - targetBrowserRect.top), Math.floor(targetBrowserRect.height - 1))); + + // Screen coordinates (approximate, good enough for synthetic events) + const screenX = Math.floor(mainBrowserWindow.screenX + targetBrowserRect.left + clientXInContent); + const screenY = Math.floor(mainBrowserWindow.screenY + targetBrowserRect.top + clientYInContent); + + return { + type: eventType, + clientX: clientXInContent, + clientY: clientYInContent, + screenX: screenX, + screenY: screenY, + button: originalEvent.button, // Typically 0 for primary + buttons: (eventType === 'mousemove' || eventType === 'mousedown') ? 1 : 0, // Primary button pressed for down/move + ctrlKey: originalEvent.ctrlKey, + altKey: originalEvent.altKey, + shiftKey: originalEvent.shiftKey, + metaKey: originalEvent.metaKey, + }; + } + mainBrowserWindow.addEventListener('mousedown', (event) => { if (event.button !== 0) return; - const gapInfo = getGapZoneInfo(event); - if (!gapInfo.isInGap || !gapInfo.targetBrowser) { - if (gapInfo.isInGap) { - logParent("Mousedown: In gap, but no specific adjacent browser identified by getGapZoneInfo."); - } + if (gapInfo.isInGap) logParent("Mousedown: In gap, but no specific adjacent browser."); return; } - - let targetBrowser = gapInfo.targetBrowser; // This is the browser physically adjacent to the gap - - // Focus/select the target browser if it's not already the selected one + let targetBrowser = gapInfo.targetBrowser; if (targetBrowser !== gBrowser.selectedBrowser) { - logParent(`Mousedown: Adjacent browser ${targetBrowser.currentURI?.spec} is not selected. Attempting to select.`); - // Ensure we are interacting with the gBrowser instance of the main window - const mainGBrowser = mainBrowserWindow.gBrowser; + const mainGBrowser = mainBrowserWindow.gBrowser; if (mainGBrowser && targetBrowser.ownerGlobal && targetBrowser.ownerGlobal.gBrowser === mainGBrowser) { const tabToSelect = mainGBrowser.getTabForBrowser(targetBrowser); if (tabToSelect && mainGBrowser.selectedTab !== tabToSelect) { mainGBrowser.selectedTab = tabToSelect; - logParent(`Mousedown: Switched selectedTab to the one for ${targetBrowser.currentURI?.spec}.`); - // After tab switch, gBrowser.selectedBrowser should update. - // Re-assign targetBrowser to be sure it's the currently selected one. - targetBrowser = mainGBrowser.selectedBrowser; - // Update browserRect as it might change after selection (e.g., if UI elements shift) - // This is important for accurate gapZoneHeight calculation. - if (targetBrowser) { // Check if targetBrowser is still valid after selection - gapInfo.browserRect = targetBrowser.getBoundingClientRect(); - } else { - logParent("Mousedown: Target browser became null after attempting selection. Aborting."); - return; - } - } else if (tabToSelect && mainGBrowser.selectedTab === tabToSelect) { - logParent(`Mousedown: Adjacent browser ${targetBrowser.currentURI?.spec} was already selected.`); - } else { - logParent(`Mousedown: Could not find tab for adjacent browser ${targetBrowser.currentURI?.spec}.`); + targetBrowser = mainGBrowser.selectedBrowser; + if (targetBrowser) gapInfo.browserRect = targetBrowser.getBoundingClientRect(); + else { logParent("Mousedown: Target browser null after selection."); return; } } - } else { - logParent(`Mousedown: Cannot determine tab for adjacent browser ${targetBrowser.currentURI?.spec}.`); } } - if (!targetBrowser || !targetBrowser.messageManager) { - logParent("Mousedown: No messageManager for target browser (either original or after selection)."); - return; + logParent("Mousedown: No messageManager for target browser."); return; } loadFrameScriptForBrowser(targetBrowser); - event.preventDefault(); - isDraggingEdgeScroll = true; - + isSynthesizingDrag = true; dragInitialModel.targetBrowserDuringDrag = targetBrowser; - dragInitialModel.gapZoneHeight = gapInfo.browserRect.height; - dragInitialModel.mouseY = event.clientY; - - const clickYInGap = event.clientY - gapInfo.browserRect.top; - const scrollPercentage = dragInitialModel.gapZoneHeight > 0 ? Math.max(0, Math.min(1, clickYInGap / dragInitialModel.gapZoneHeight)) : 0; - dragInitialModel.initialScrollPercentageOnDragStart = scrollPercentage; - logParent(`Mousedown on ${targetBrowser.currentURI?.spec}: gapHeight=${dragInitialModel.gapZoneHeight.toFixed(2)}, clickYInGap=${clickYInGap.toFixed(2)}, initialMouseY=${dragInitialModel.mouseY.toFixed(2)}, initialScrollPerc=${scrollPercentage.toFixed(4)}`); + const eventData = createSyntheticEventData(event, gapInfo.browserRect, 'mousedown'); + logParent(`Mousedown: Sending SynthesizeMouseEvent (mousedown) to ${targetBrowser.currentURI?.spec} at X:${eventData.clientX}, Y:${eventData.clientY}`); + targetBrowser.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "SynthesizeMouseEvent", eventData); - targetBrowser.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "ScrollToPercentage", { percentage: scrollPercentage }); - - mainBrowserWindow.addEventListener('mousemove', handleEdgeScrollDrag, true); - mainBrowserWindow.addEventListener('mouseup', handleEdgeScrollEnd, true); + mainBrowserWindow.addEventListener('mousemove', handleSyntheticDrag, true); + mainBrowserWindow.addEventListener('mouseup', handleSyntheticDragEnd, true); }, true); - function handleEdgeScrollDrag(event) { - if (!isDraggingEdgeScroll || !dragInitialModel.targetBrowserDuringDrag) return; - + function handleSyntheticDrag(event) { + if (!isSynthesizingDrag || !dragInitialModel.targetBrowserDuringDrag) return; const targetBrowser = dragInitialModel.targetBrowserDuringDrag; - // Ensure the browser being dragged is still the selected one, and it's valid if (gBrowser.selectedBrowser !== targetBrowser || !targetBrowser.messageManager) { - logParent("Drag: Target browser changed, lost messageManager, or no contentWindow. Ending drag."); - handleEdgeScrollEnd(event); // Pass event for proper cleanup if needed - return; + logParent("Drag: Target browser changed or lost messageManager. Ending drag."); + handleSyntheticDragEnd(event); return; } - event.preventDefault(); - event.stopPropagation(); - - const deltaYFromInitialMouseY = event.clientY - dragInitialModel.mouseY; - let newScrollPercentage; - - if (dragInitialModel.gapZoneHeight > 0) { - const percentageDelta = deltaYFromInitialMouseY / dragInitialModel.gapZoneHeight; - newScrollPercentage = dragInitialModel.initialScrollPercentageOnDragStart + percentageDelta; - newScrollPercentage = Math.max(0, Math.min(1, newScrollPercentage)); - // logParent(`Drag: clientY=${event.clientY.toFixed(2)}, deltaMouseY=${deltaYFromInitialMouseY.toFixed(2)}, percDelta=${percentageDelta.toFixed(4)}, newScrollPerc=${newScrollPercentage.toFixed(4)}`); - } else { - logParent("Drag: Error - gapZoneHeight is 0 or invalid. Cannot calculate scroll percentage."); - handleEdgeScrollEnd(event); // End drag if calculation is impossible - return; + event.preventDefault(); event.stopPropagation(); + // Important: browserRect might change if window resizes or other UI shifts. Re-get it. + const currentTargetBrowserRect = targetBrowser.getBoundingClientRect(); + if (currentTargetBrowserRect.width === 0 || currentTargetBrowserRect.height === 0) { + logParent("Drag: Target browser rect is zero. Ending drag."); + handleSyntheticDragEnd(event); return; } - - targetBrowser.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "ScrollToPercentage", { percentage: newScrollPercentage }); + const eventData = createSyntheticEventData(event, currentTargetBrowserRect, 'mousemove'); + // logParent(`Drag: Sending SynthesizeMouseEvent (mousemove) to ${targetBrowser.currentURI?.spec} at X:${eventData.clientX}, Y:${eventData.clientY}`); + targetBrowser.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "SynthesizeMouseEvent", eventData); } - function handleEdgeScrollEnd(event) { - if (isDraggingEdgeScroll && dragInitialModel.targetBrowserDuringDrag) { - if (event) { - event.preventDefault(); - event.stopPropagation(); + function handleSyntheticDragEnd(event) { + if (isSynthesizingDrag && dragInitialModel.targetBrowserDuringDrag) { + const targetBrowser = dragInitialModel.targetBrowserDuringDrag; + if (targetBrowser.messageManager) { + if (event) { // If called by an event + event.preventDefault(); event.stopPropagation(); + const currentTargetBrowserRect = targetBrowser.getBoundingClientRect(); + if (currentTargetBrowserRect.width > 0 && currentTargetBrowserRect.height > 0) { + const eventData = createSyntheticEventData(event, currentTargetBrowserRect, 'mouseup'); + logParent(`DragEnd: Sending SynthesizeMouseEvent (mouseup) to ${targetBrowser.currentURI?.spec} at X:${eventData.clientX}, Y:${eventData.clientY}`); + targetBrowser.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "SynthesizeMouseEvent", eventData); + } else { + logParent("DragEnd: Target browser rect is zero, cannot send mouseup."); + } + } else { + logParent("DragEnd: Called without event, mouseup not synthesized."); + } } - // logParent("DragEnd: Releasing drag."); } - - isDraggingEdgeScroll = false; + isSynthesizingDrag = false; dragInitialModel.targetBrowserDuringDrag = null; - - mainBrowserWindow.removeEventListener('mousemove', handleEdgeScrollDrag, true); - mainBrowserWindow.removeEventListener('mouseup', handleEdgeScrollEnd, true); + mainBrowserWindow.removeEventListener('mousemove', handleSyntheticDrag, true); + mainBrowserWindow.removeEventListener('mouseup', handleSyntheticDragEnd, true); } mainBrowserWindow.addEventListener('wheel', (event) => { - // For wheel events, we can use a similar logic to find the target browser - // or decide if wheel events in the gap should always target the *currently selected* rightmost browser. - // For now, let's keep the wheel targeting logic simpler: it targets the *selected* browser if it's rightmost. - // If you want wheel to also switch focus, getGapZoneInfo would need to be called here too, - // and focus switching logic similar to mousedown would be needed. - const activeBrowser = gBrowser.selectedBrowser; if (!activeBrowser) return; - const windowWidth = mainBrowserWindow.innerWidth; const activeBrowserRect = activeBrowser.getBoundingClientRect(); const isActiveBrowserRightmost = (windowWidth - activeBrowserRect.right) < RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX; const isInFarRightWindowEdgeGap = event.clientX > (windowWidth - EDGE_INTERACTION_WIDTH_PX) && event.clientX < windowWidth; - if (!isActiveBrowserRightmost || !isInFarRightWindowEdgeGap) return; - - const targetBrowser = activeBrowser; // Wheel targets the active browser if it's rightmost and mouse is in gap - - if (!targetBrowser.messageManager) { - logParent("Wheel: No messageManager for target browser."); - return; - } + const targetBrowser = activeBrowser; + if (!targetBrowser.messageManager) { logParent("Wheel: No messageManager."); return; } loadFrameScriptForBrowser(targetBrowser); - - event.preventDefault(); - event.stopPropagation(); - + event.preventDefault(); event.stopPropagation(); const wheelData = { deltaX: event.deltaX, deltaY: event.deltaY, deltaZ: event.deltaZ, deltaMode: event.deltaMode, ctrlKey: event.ctrlKey, altKey: event.altKey, shiftKey: event.shiftKey, metaKey: event.metaKey, + // For wheel, we also need clientX/Y for the frame script to target the event + clientX: Math.max(0, Math.floor(activeBrowserRect.width - SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE)), + clientY: Math.max(0, Math.min(Math.floor(event.clientY - activeBrowserRect.top), Math.floor(activeBrowserRect.height - 1))) }; + logParent(`Wheel: Sending DispatchWheel to ${targetBrowser.currentURI?.spec} with deltaX:${wheelData.deltaX}, deltaY:${wheelData.deltaY}, clientX:${wheelData.clientX}, clientY:${wheelData.clientY}`); targetBrowser.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "DispatchWheel", { wheelData }); }, { capture: true, passive: false }); @@ -285,6 +220,5 @@ } else { mainBrowserWindow.addEventListener("load", initFrameScripts, { once: true }); } - - logParent("Edge Scroll Handler Initialized (IPC for Synthetic Events)"); + logParent("Edge Scroll Handler Initialized (Synthesizing Mouse Events for Drag)"); })(); \ No newline at end of file From a4ad40530486f39f0c0ca2f6a4765d83ab290ee2 Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Wed, 21 May 2025 00:24:31 +0800 Subject: [PATCH 06/34] Synthesize mouse click event --- src/zen/common/ZenEdgeScrollFrame.js | 134 ++++++++++++++++--------- src/zen/common/ZenEdgeScrollHandler.js | 4 +- 2 files changed, 88 insertions(+), 50 deletions(-) diff --git a/src/zen/common/ZenEdgeScrollFrame.js b/src/zen/common/ZenEdgeScrollFrame.js index 6b80608610..deacb1a36c 100644 --- a/src/zen/common/ZenEdgeScrollFrame.js +++ b/src/zen/common/ZenEdgeScrollFrame.js @@ -1,9 +1,11 @@ /* eslint-env mozilla/frame-script */ +/* global content, sendAsyncMessage, addMessageListener, Components */ // For linter + +const { utils: Cu, interfaces: Ci } = Components; function logFrame(message) { - // Use dump for more reliable output from frame scripts to the system console + dump("ZenEdgeScrollFrame: " + message + "\n"); console.log("ZenEdgeScrollFrame: " + message + "\n"); - // console.log("ZenEdgeScrollFrame: " + message + "\n"); // Can also be used, output goes to Browser Console } logFrame("Frame script loaded for: " + (content && content.document ? content.document.location.href : "unknown content location")); @@ -16,43 +18,66 @@ addMessageListener("ZenEdgeScroll:ScrollToPercentage", function(message) { */ addMessageListener("ZenEdgeScroll:SynthesizeMouseEvent", function(message) { + logFrame("SynthesizeMouseEvent listener triggered. Current content.document.location.href = " + (content && content.document ? content.document.location.href : "unknown or error")); + logFrame("!!! FRAME: SynthesizeMouseEvent RECEIVED! Data: " + JSON.stringify(message.data)); const data = message.data; if (!data || !data.type) { logFrame("SynthesizeMouseEvent: Invalid data received."); return; } - // clientX, clientY are relative to the content viewport, calculated by parent - // For scrollbar interaction, dispatching on documentElement or scrollingElement is usually effective. - const targetElement = content.document.elementFromPoint(data.clientX, data.clientY) || content.document.documentElement || content.document.body; - - if (!targetElement) { - logFrame(`SynthesizeMouseEvent: No target element found at (${data.clientX}, ${data.clientY}) for ${data.type}`); + if (!content.windowUtils) { + logFrame("SynthesizeMouseEvent: content.windowUtils is not available. Cannot send trusted event."); + // Fallback to standard dispatchEvent (which will be untrusted) or do nothing + // For now, let's just log and return if windowUtils is missing. return; } - // logFrame(`SynthesizeMouseEvent: Dispatching ${data.type} at X:${data.clientX}, Y:${data.clientY} on ${targetElement.tagName}`); + + let modifiers = 0; + if (data.altKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_ALT; + if (data.ctrlKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_CONTROL; + if (data.metaKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_META; + if (data.shiftKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_SHIFT; + + // The 'buttons' property (plural) reflects the state of all buttons during the event. + // nsIDOMWindowUtils.sendMouseEvent uses the 'button' (singular) for the primary button causing the event, + // and modifiers can also reflect button states. + // For dragging, the sequence of mousedown (button 0), mousemove (button 0 still considered down), mouseup (button 0) + // is important. The 'buttons' property from the parent is trying to reflect this. + // We need to ensure the modifier flags for sendMouseEvent correctly reflect this if necessary. + // However, for sendMouseEvent, the primary button state is often handled by the `aButton` argument. + // If `data.buttons` is 1 (primary button down), we can add that to modifiers for mousemove. + if (data.type === "mousemove" && data.buttons === 1) { + modifiers |= Ci.nsIDOMWindowUtils.BUTTON_PRIMARY_ACTION; + } + + + let clickCount = 0; + if (data.type === "mousedown" || data.type === "mouseup") { + clickCount = 1; // Standard for a single click part + } + + // logFrame(`SynthesizeMouseEvent (Trusted): Dispatching ${data.type} at X:${data.clientX}, Y:${data.clientY} with button:${data.button}, buttons:${data.buttons}, clickCount:${clickCount}, modifiers:${modifiers}`); try { - const syntheticEvent = new content.MouseEvent(data.type, { - bubbles: true, - cancelable: (data.type !== 'mousemove'), // mousemove is often not cancelable - composed: true, - view: content, // Essential: the content window - detail: (data.type === 'mousedown' || data.type === 'mouseup' || data.type === 'click') ? 1 : 0, - screenX: data.screenX, - screenY: data.screenY, - clientX: data.clientX, - clientY: data.clientY, - ctrlKey: data.ctrlKey, - altKey: data.altKey, - shiftKey: data.shiftKey, - metaKey: data.metaKey, - button: data.button, - buttons: data.buttons, // Crucial for dragging state - }); - targetElement.dispatchEvent(syntheticEvent); + // Parameters for sendMouseEvent: + // aType, aX, aY, aButton, aClickCount, aModifiers, aIgnoreRootScrollFrame, + // aPressure (0.0 to 1.0, 0.5 for mouse), aInputSource (one of INPUT_SOURCE_*), aIsSynthesized (false for trusted) + content.windowUtils.sendMouseEvent( + data.type, // e.g., "mousedown", "mousemove", "mouseup" + data.clientX, // X coordinate relative to the content window's viewport + data.clientY, // Y coordinate relative to the content window's viewport + data.button, // The button number (0 for left, 1 for middle, 2 for right) + clickCount, // Click count + modifiers, // Modifier keys + false, // aIgnoreRootScrollFrame (false means interact with page scroll) + 0.5, // aPressure (0.5 is typical for mouse) + Ci.nsIDOMWindowUtils.INPUT_SOURCE_MOUSE, // Input source + false // aIsSynthesized (false makes event.isTrusted = true) + ); + // logFrame(`SynthesizeMouseEvent (Trusted): Dispatched ${data.type} successfully.`); } catch (e) { - logFrame(`Error dispatching synthetic ${data.type} event: ${e} - ${e.stack}`); + logFrame(`Error dispatching trusted synthetic ${data.type} event: ${e} - ${e.stack}`); } }); @@ -64,29 +89,42 @@ addMessageListener("ZenEdgeScroll:DispatchWheel", function(message) { logFrame("DispatchWheel: No eventData received."); return; } + if (!content.windowUtils) { + logFrame("DispatchWheel: content.windowUtils is not available. Cannot send trusted wheel event."); + return; + } - // Use clientX/Y from eventData if provided, otherwise fallback const clientX = typeof eventData.clientX === 'number' ? eventData.clientX : (doc.documentElement.clientWidth / 2); const clientY = typeof eventData.clientY === 'number' ? eventData.clientY : (doc.documentElement.clientHeight / 2); - const targetElement = doc.elementFromPoint(clientX, clientY) || doc.documentElement || doc.body; - - if (targetElement) { - // logFrame(`DispatchWheel: Dispatching on ${targetElement.tagName} at X:${clientX}, Y:${clientY}`); - try { - const clonedWheelEvent = new content.WheelEvent("wheel", { - deltaX: eventData.deltaX || 0, deltaY: eventData.deltaY || 0, deltaZ: eventData.deltaZ || 0, - deltaMode: eventData.deltaMode || 0, bubbles: true, cancelable: true, composed: true, view: content, - ctrlKey: eventData.ctrlKey, altKey: eventData.altKey, shiftKey: eventData.shiftKey, metaKey: eventData.metaKey, - clientX: clientX, clientY: clientY, // Include clientX/Y in the event itself - screenX: eventData.screenX, screenY: eventData.screenY, // If available - button: 0, buttons: 0, // Wheel events typically don't have button presses - }); - targetElement.dispatchEvent(clonedWheelEvent); - } catch (e) { - logFrame(`Error dispatching wheel event: ${e} - ${e.stack}`); - } - } else { - logFrame("DispatchWheel: No targetElement found."); + let modifiers = 0; + if (eventData.altKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_ALT; + if (eventData.ctrlKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_CONTROL; + if (eventData.metaKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_META; + if (eventData.shiftKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_SHIFT; + + // logFrame(`DispatchWheel (Trusted): Dispatching at X:${clientX}, Y:${clientY} with deltaY:${eventData.deltaY}`); + try { + // Parameters for sendWheelEvent: + // aX, aY, aDeltaX, aDeltaY, aDeltaZ, aDeltaMode, aModifiers, aLineOrPageDeltaX, aLineOrPageDeltaY, + // aIsNoLineOrPageDelta, aIgnoreRootScrollFrame, aIsMomentum, aIsFromTouch, aIsSynthesized + content.windowUtils.sendWheelEvent( + clientX, + clientY, + eventData.deltaX, + eventData.deltaY, + eventData.deltaZ, + eventData.deltaMode, + modifiers, + 0, // aLineOrPageDeltaX (not typically needed if pixel deltas are provided) + 0, // aLineOrPageDeltaY + true, // aIsNoLineOrPageDelta (true if line/page deltas are not provided) + false, // aIgnoreRootScrollFrame + false, // aIsMomentum + false, // aIsFromTouch + false // aIsSynthesized (false makes event.isTrusted = true) + ); + } catch (e) { + logFrame(`Error dispatching trusted wheel event: ${e} - ${e.stack}`); } }); \ No newline at end of file diff --git a/src/zen/common/ZenEdgeScrollHandler.js b/src/zen/common/ZenEdgeScrollHandler.js index 44116aeb4b..981e208bcc 100644 --- a/src/zen/common/ZenEdgeScrollHandler.js +++ b/src/zen/common/ZenEdgeScrollHandler.js @@ -10,7 +10,7 @@ const EDGE_INTERACTION_WIDTH_PX = 20; const RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX = 25; // How many pixels from the right edge of the content viewport to target the synthetic click - const SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE = 8; // Aim for the scrollbar track + const SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE = 2; // Aim for the scrollbar track const mainBrowserWindow = window; let isSynthesizingDrag = false; // Changed from isDraggingEdgeScroll @@ -67,7 +67,7 @@ let potentialTargetBrowserRect = null; if (gBrowser && gBrowser.browsers) { for (const browser of gBrowser.browsers) { - if (browser.hidden) continue; + if (browser.hidden || browser.getAttribute("transparent") || !browser.getAttribute("primary") ) continue; const browserRect = browser.getBoundingClientRect(); if (browserRect.width === 0 || browserRect.height === 0) continue; const isBrowserAtRightEdge = (windowWidth - browserRect.right) < RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX && browserRect.right > (windowWidth - EDGE_INTERACTION_WIDTH_PX - RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX); From 416db95a3e61fbdc44ed74b38d95db8abc8b9a53 Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Wed, 21 May 2025 18:38:03 +0800 Subject: [PATCH 07/34] Working actor implementation --- .formal-git/components | 3 +- src/browser/base/content/zen-assets.inc.xhtml | 1 + .../base/content/zen-assets.jar.inc.mn | 6 +- src/zen/common/ZenEdgeScrollFrame.js | 130 ------- src/zen/common/ZenEdgeScrollHandler.js | 224 ------------ src/zen/common/ZenStartup.mjs | 13 - src/zen/edgescroll/ZenEdgeScrollManager.mjs | 318 ++++++++++++++++++ .../actors/ZenEdgeScrollChild.sys.mjs | 107 ++++++ .../actors/ZenEdgeScrollParent.sys.mjs | 36 ++ src/zen/edgescroll/moz.build | 10 + src/zen/moz.build | 1 + 11 files changed, 479 insertions(+), 370 deletions(-) delete mode 100644 src/zen/common/ZenEdgeScrollFrame.js delete mode 100644 src/zen/common/ZenEdgeScrollHandler.js create mode 100644 src/zen/edgescroll/ZenEdgeScrollManager.mjs create mode 100644 src/zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs create mode 100644 src/zen/edgescroll/actors/ZenEdgeScrollParent.sys.mjs create mode 100644 src/zen/edgescroll/moz.build diff --git a/.formal-git/components b/.formal-git/components index 43efe01f52..ddb6a85737 100644 --- a/.formal-git/components +++ b/.formal-git/components @@ -17,4 +17,5 @@ scripts workflows winsign flatpak -configs \ No newline at end of file +configs +edgescroll \ No newline at end of file diff --git a/src/browser/base/content/zen-assets.inc.xhtml b/src/browser/base/content/zen-assets.inc.xhtml index a22c866350..7032c5ca66 100644 --- a/src/browser/base/content/zen-assets.inc.xhtml +++ b/src/browser/base/content/zen-assets.inc.xhtml @@ -44,4 +44,5 @@ Services.scriptloader.loadSubScript("chrome://browser/content/zen-components/Zen Services.scriptloader.loadSubScript("chrome://browser/content/zen-components/ZenGlanceManager.mjs", this); Services.scriptloader.loadSubScript("chrome://browser/content/zen-components/ZenMediaController.mjs", this); Services.scriptloader.loadSubScript("chrome://browser/content/zen-components/ZenDownloadAnimation.mjs", this); +Services.scriptloader.loadSubScript("chrome://browser/content/zen-components/ZenEdgeScrollManager.mjs", this); diff --git a/src/browser/base/content/zen-assets.jar.inc.mn b/src/browser/base/content/zen-assets.jar.inc.mn index f6c819236c..76a7005106 100644 --- a/src/browser/base/content/zen-assets.jar.inc.mn +++ b/src/browser/base/content/zen-assets.jar.inc.mn @@ -3,8 +3,6 @@ content/browser/zenThemeModifier.js (../../zen/common/zenThemeModifier.js) content/browser/ZenStartup.mjs (../../zen/common/ZenStartup.mjs) content/browser/zen-sets.js (../../zen/common/zen-sets.js) - content/browser/ZenEdgeScrollHandler.js (../../zen/common/ZenEdgeScrollHandler.js) - content/browser/ZenEdgeScrollFrame.js (../../zen/common/ZenEdgeScrollFrame.js) content/browser/ZenUIManager.mjs (../../zen/common/ZenUIManager.mjs) content/browser/zen-components/ZenActorsManager.mjs (../../zen/common/ZenActorsManager.mjs) content/browser/zen-components/ZenEmojies.mjs (../../zen/common/ZenEmojies.mjs) @@ -63,6 +61,10 @@ content/browser/zen-components/actors/ZenGlanceChild.sys.mjs (../../zen/glance/actors/ZenGlanceChild.sys.mjs) content/browser/zen-components/actors/ZenGlanceParent.sys.mjs (../../zen/glance/actors/ZenGlanceParent.sys.mjs) + content/browser/zen-components/ZenEdgeScrollManager.mjs (../../zen/edgescroll/ZenEdgeScrollManager.mjs) + content/browser/zen-components/actors/ZenEdgeScrollChild.sys.mjs (../../zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs) + content/browser/zen-components/actors/ZenEdgeScrollParent.sys.mjs (../../zen/edgescroll/actors/ZenEdgeScrollParent.sys.mjs) + content/browser/zen-components/ZenFolders.mjs (../../zen/folders/ZenFolders.mjs) content/browser/zen-styles/zen-folders.css (../../zen/folders/zen-folders.css) diff --git a/src/zen/common/ZenEdgeScrollFrame.js b/src/zen/common/ZenEdgeScrollFrame.js deleted file mode 100644 index deacb1a36c..0000000000 --- a/src/zen/common/ZenEdgeScrollFrame.js +++ /dev/null @@ -1,130 +0,0 @@ -/* eslint-env mozilla/frame-script */ -/* global content, sendAsyncMessage, addMessageListener, Components */ // For linter - -const { utils: Cu, interfaces: Ci } = Components; - -function logFrame(message) { - dump("ZenEdgeScrollFrame: " + message + "\n"); - console.log("ZenEdgeScrollFrame: " + message + "\n"); -} - -logFrame("Frame script loaded for: " + (content && content.document ? content.document.location.href : "unknown content location")); - -// REMOVE or COMMENT OUT the old ScrollToPercentage listener -/* -addMessageListener("ZenEdgeScroll:ScrollToPercentage", function(message) { - // ... old code ... -}); -*/ - -addMessageListener("ZenEdgeScroll:SynthesizeMouseEvent", function(message) { - logFrame("SynthesizeMouseEvent listener triggered. Current content.document.location.href = " + (content && content.document ? content.document.location.href : "unknown or error")); - logFrame("!!! FRAME: SynthesizeMouseEvent RECEIVED! Data: " + JSON.stringify(message.data)); - const data = message.data; - if (!data || !data.type) { - logFrame("SynthesizeMouseEvent: Invalid data received."); - return; - } - - if (!content.windowUtils) { - logFrame("SynthesizeMouseEvent: content.windowUtils is not available. Cannot send trusted event."); - // Fallback to standard dispatchEvent (which will be untrusted) or do nothing - // For now, let's just log and return if windowUtils is missing. - return; - } - - let modifiers = 0; - if (data.altKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_ALT; - if (data.ctrlKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_CONTROL; - if (data.metaKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_META; - if (data.shiftKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_SHIFT; - - // The 'buttons' property (plural) reflects the state of all buttons during the event. - // nsIDOMWindowUtils.sendMouseEvent uses the 'button' (singular) for the primary button causing the event, - // and modifiers can also reflect button states. - // For dragging, the sequence of mousedown (button 0), mousemove (button 0 still considered down), mouseup (button 0) - // is important. The 'buttons' property from the parent is trying to reflect this. - // We need to ensure the modifier flags for sendMouseEvent correctly reflect this if necessary. - // However, for sendMouseEvent, the primary button state is often handled by the `aButton` argument. - // If `data.buttons` is 1 (primary button down), we can add that to modifiers for mousemove. - if (data.type === "mousemove" && data.buttons === 1) { - modifiers |= Ci.nsIDOMWindowUtils.BUTTON_PRIMARY_ACTION; - } - - - let clickCount = 0; - if (data.type === "mousedown" || data.type === "mouseup") { - clickCount = 1; // Standard for a single click part - } - - // logFrame(`SynthesizeMouseEvent (Trusted): Dispatching ${data.type} at X:${data.clientX}, Y:${data.clientY} with button:${data.button}, buttons:${data.buttons}, clickCount:${clickCount}, modifiers:${modifiers}`); - - try { - // Parameters for sendMouseEvent: - // aType, aX, aY, aButton, aClickCount, aModifiers, aIgnoreRootScrollFrame, - // aPressure (0.0 to 1.0, 0.5 for mouse), aInputSource (one of INPUT_SOURCE_*), aIsSynthesized (false for trusted) - content.windowUtils.sendMouseEvent( - data.type, // e.g., "mousedown", "mousemove", "mouseup" - data.clientX, // X coordinate relative to the content window's viewport - data.clientY, // Y coordinate relative to the content window's viewport - data.button, // The button number (0 for left, 1 for middle, 2 for right) - clickCount, // Click count - modifiers, // Modifier keys - false, // aIgnoreRootScrollFrame (false means interact with page scroll) - 0.5, // aPressure (0.5 is typical for mouse) - Ci.nsIDOMWindowUtils.INPUT_SOURCE_MOUSE, // Input source - false // aIsSynthesized (false makes event.isTrusted = true) - ); - // logFrame(`SynthesizeMouseEvent (Trusted): Dispatched ${data.type} successfully.`); - } catch (e) { - logFrame(`Error dispatching trusted synthetic ${data.type} event: ${e} - ${e.stack}`); - } -}); - - -addMessageListener("ZenEdgeScroll:DispatchWheel", function(message) { - const doc = content.document; - const eventData = message.data.wheelData; - if (!eventData) { - logFrame("DispatchWheel: No eventData received."); - return; - } - if (!content.windowUtils) { - logFrame("DispatchWheel: content.windowUtils is not available. Cannot send trusted wheel event."); - return; - } - - const clientX = typeof eventData.clientX === 'number' ? eventData.clientX : (doc.documentElement.clientWidth / 2); - const clientY = typeof eventData.clientY === 'number' ? eventData.clientY : (doc.documentElement.clientHeight / 2); - - let modifiers = 0; - if (eventData.altKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_ALT; - if (eventData.ctrlKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_CONTROL; - if (eventData.metaKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_META; - if (eventData.shiftKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_SHIFT; - - // logFrame(`DispatchWheel (Trusted): Dispatching at X:${clientX}, Y:${clientY} with deltaY:${eventData.deltaY}`); - try { - // Parameters for sendWheelEvent: - // aX, aY, aDeltaX, aDeltaY, aDeltaZ, aDeltaMode, aModifiers, aLineOrPageDeltaX, aLineOrPageDeltaY, - // aIsNoLineOrPageDelta, aIgnoreRootScrollFrame, aIsMomentum, aIsFromTouch, aIsSynthesized - content.windowUtils.sendWheelEvent( - clientX, - clientY, - eventData.deltaX, - eventData.deltaY, - eventData.deltaZ, - eventData.deltaMode, - modifiers, - 0, // aLineOrPageDeltaX (not typically needed if pixel deltas are provided) - 0, // aLineOrPageDeltaY - true, // aIsNoLineOrPageDelta (true if line/page deltas are not provided) - false, // aIgnoreRootScrollFrame - false, // aIsMomentum - false, // aIsFromTouch - false // aIsSynthesized (false makes event.isTrusted = true) - ); - } catch (e) { - logFrame(`Error dispatching trusted wheel event: ${e} - ${e.stack}`); - } -}); \ No newline at end of file diff --git a/src/zen/common/ZenEdgeScrollHandler.js b/src/zen/common/ZenEdgeScrollHandler.js deleted file mode 100644 index 981e208bcc..0000000000 --- a/src/zen/common/ZenEdgeScrollHandler.js +++ /dev/null @@ -1,224 +0,0 @@ -(function() { - if (window.gEdgeScrollHandlerInitialized) { - return; - } - window.gEdgeScrollHandlerInitialized = true; - - const FRAME_SCRIPT_URL = "chrome://browser/content/ZenEdgeScrollFrame.js"; - const MESSAGE_PREFIX = "ZenEdgeScroll:"; - - const EDGE_INTERACTION_WIDTH_PX = 20; - const RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX = 25; - // How many pixels from the right edge of the content viewport to target the synthetic click - const SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE = 2; // Aim for the scrollbar track - - const mainBrowserWindow = window; - let isSynthesizingDrag = false; // Changed from isDraggingEdgeScroll - let dragInitialModel = { - targetBrowserDuringDrag: null, - // No scroll-percentage specific model needed here, but we store the target browser - }; - - function logParent(message) { - // console.log("EdgeScrollHandler (Parent): " + message); - dump("EdgeScrollHandler (Parent): " + message + "\n"); - } - - function loadFrameScriptForBrowser(browser) { - if (browser && browser.messageManager && !browser.frameScriptLoadedForEdgeScroll) { - try { - browser.messageManager.loadFrameScript(FRAME_SCRIPT_URL, false); - browser.frameScriptLoadedForEdgeScroll = true; - } catch (e) { - console.error("EdgeScrollHandler (Parent): CRITICAL ERROR loading frame script:", FRAME_SCRIPT_URL, e); - dump(`EdgeScrollHandler (Parent): CRITICAL ERROR loading frame script: ${FRAME_SCRIPT_URL} - ${e} - ${e.stack}\n`); - } - } - } - - function initFrameScripts() { - if (!gBrowser || typeof gBrowser.tabs === 'undefined' || !gBrowser.tabs.length) { - if (!window.gEdgeScrollInitRetryCount || window.gEdgeScrollInitRetryCount < 5) { - window.gEdgeScrollInitRetryCount = (window.gEdgeScrollInitRetryCount || 0) + 1; - setTimeout(initFrameScripts, 500 * window.gEdgeScrollInitRetryCount); - } - return; - } - for (const tab of gBrowser.tabs) { - if (tab.linkedBrowser) loadFrameScriptForBrowser(tab.linkedBrowser); - } - gBrowser.tabContainer.addEventListener("TabOpen", event => { - if (event.target.linkedBrowser) loadFrameScriptForBrowser(event.target.linkedBrowser); - }); - gBrowser.tabContainer.addEventListener("TabSelect", event => { - if (event.target.linkedBrowser) loadFrameScriptForBrowser(event.target.linkedBrowser); - }); - if (gBrowser.selectedBrowser) loadFrameScriptForBrowser(gBrowser.selectedBrowser); - } - - function getGapZoneInfo(event) { - const windowWidth = mainBrowserWindow.innerWidth; - const eventClientX = event.clientX; - const eventClientY = event.clientY; - const isInFarRightWindowEdgeGap = eventClientX > (windowWidth - EDGE_INTERACTION_WIDTH_PX) && eventClientX < windowWidth; - if (!isInFarRightWindowEdgeGap) return { isInGap: false, targetBrowser: null, browserRect: null }; - - let potentialTargetBrowser = null; - let potentialTargetBrowserRect = null; - if (gBrowser && gBrowser.browsers) { - for (const browser of gBrowser.browsers) { - if (browser.hidden || browser.getAttribute("transparent") || !browser.getAttribute("primary") ) continue; - const browserRect = browser.getBoundingClientRect(); - if (browserRect.width === 0 || browserRect.height === 0) continue; - const isBrowserAtRightEdge = (windowWidth - browserRect.right) < RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX && browserRect.right > (windowWidth - EDGE_INTERACTION_WIDTH_PX - RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX); - const isEventYWithinBrowser = eventClientY >= browserRect.top && eventClientY <= browserRect.bottom; - if (isBrowserAtRightEdge && isEventYWithinBrowser) { - potentialTargetBrowser = browser; - potentialTargetBrowserRect = browserRect; - break; - } - } - } - if (potentialTargetBrowser) { - return { isInGap: true, targetBrowser: potentialTargetBrowser, browserRect: potentialTargetBrowserRect }; - } - return { isInGap: true, targetBrowser: null, browserRect: null }; - } - - // Helper to create common event data for IPC - function createSyntheticEventData(originalEvent, targetBrowserRect, eventType) { - // Coordinates relative to the target browser's viewport - // Horizontal position: fixed, near the right edge (scrollbar) - const clientXInContent = Math.max(0, Math.floor(targetBrowserRect.width - SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE)); - // Vertical position: mirrors the original event's Y relative to the browser's top - const clientYInContent = Math.max(0, Math.min(Math.floor(originalEvent.clientY - targetBrowserRect.top), Math.floor(targetBrowserRect.height - 1))); - - // Screen coordinates (approximate, good enough for synthetic events) - const screenX = Math.floor(mainBrowserWindow.screenX + targetBrowserRect.left + clientXInContent); - const screenY = Math.floor(mainBrowserWindow.screenY + targetBrowserRect.top + clientYInContent); - - return { - type: eventType, - clientX: clientXInContent, - clientY: clientYInContent, - screenX: screenX, - screenY: screenY, - button: originalEvent.button, // Typically 0 for primary - buttons: (eventType === 'mousemove' || eventType === 'mousedown') ? 1 : 0, // Primary button pressed for down/move - ctrlKey: originalEvent.ctrlKey, - altKey: originalEvent.altKey, - shiftKey: originalEvent.shiftKey, - metaKey: originalEvent.metaKey, - }; - } - - mainBrowserWindow.addEventListener('mousedown', (event) => { - if (event.button !== 0) return; - const gapInfo = getGapZoneInfo(event); - if (!gapInfo.isInGap || !gapInfo.targetBrowser) { - if (gapInfo.isInGap) logParent("Mousedown: In gap, but no specific adjacent browser."); - return; - } - let targetBrowser = gapInfo.targetBrowser; - if (targetBrowser !== gBrowser.selectedBrowser) { - const mainGBrowser = mainBrowserWindow.gBrowser; - if (mainGBrowser && targetBrowser.ownerGlobal && targetBrowser.ownerGlobal.gBrowser === mainGBrowser) { - const tabToSelect = mainGBrowser.getTabForBrowser(targetBrowser); - if (tabToSelect && mainGBrowser.selectedTab !== tabToSelect) { - mainGBrowser.selectedTab = tabToSelect; - targetBrowser = mainGBrowser.selectedBrowser; - if (targetBrowser) gapInfo.browserRect = targetBrowser.getBoundingClientRect(); - else { logParent("Mousedown: Target browser null after selection."); return; } - } - } - } - if (!targetBrowser || !targetBrowser.messageManager) { - logParent("Mousedown: No messageManager for target browser."); return; - } - loadFrameScriptForBrowser(targetBrowser); - event.preventDefault(); - isSynthesizingDrag = true; - dragInitialModel.targetBrowserDuringDrag = targetBrowser; - - const eventData = createSyntheticEventData(event, gapInfo.browserRect, 'mousedown'); - logParent(`Mousedown: Sending SynthesizeMouseEvent (mousedown) to ${targetBrowser.currentURI?.spec} at X:${eventData.clientX}, Y:${eventData.clientY}`); - targetBrowser.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "SynthesizeMouseEvent", eventData); - - mainBrowserWindow.addEventListener('mousemove', handleSyntheticDrag, true); - mainBrowserWindow.addEventListener('mouseup', handleSyntheticDragEnd, true); - }, true); - - function handleSyntheticDrag(event) { - if (!isSynthesizingDrag || !dragInitialModel.targetBrowserDuringDrag) return; - const targetBrowser = dragInitialModel.targetBrowserDuringDrag; - if (gBrowser.selectedBrowser !== targetBrowser || !targetBrowser.messageManager) { - logParent("Drag: Target browser changed or lost messageManager. Ending drag."); - handleSyntheticDragEnd(event); return; - } - event.preventDefault(); event.stopPropagation(); - // Important: browserRect might change if window resizes or other UI shifts. Re-get it. - const currentTargetBrowserRect = targetBrowser.getBoundingClientRect(); - if (currentTargetBrowserRect.width === 0 || currentTargetBrowserRect.height === 0) { - logParent("Drag: Target browser rect is zero. Ending drag."); - handleSyntheticDragEnd(event); return; - } - const eventData = createSyntheticEventData(event, currentTargetBrowserRect, 'mousemove'); - // logParent(`Drag: Sending SynthesizeMouseEvent (mousemove) to ${targetBrowser.currentURI?.spec} at X:${eventData.clientX}, Y:${eventData.clientY}`); - targetBrowser.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "SynthesizeMouseEvent", eventData); - } - - function handleSyntheticDragEnd(event) { - if (isSynthesizingDrag && dragInitialModel.targetBrowserDuringDrag) { - const targetBrowser = dragInitialModel.targetBrowserDuringDrag; - if (targetBrowser.messageManager) { - if (event) { // If called by an event - event.preventDefault(); event.stopPropagation(); - const currentTargetBrowserRect = targetBrowser.getBoundingClientRect(); - if (currentTargetBrowserRect.width > 0 && currentTargetBrowserRect.height > 0) { - const eventData = createSyntheticEventData(event, currentTargetBrowserRect, 'mouseup'); - logParent(`DragEnd: Sending SynthesizeMouseEvent (mouseup) to ${targetBrowser.currentURI?.spec} at X:${eventData.clientX}, Y:${eventData.clientY}`); - targetBrowser.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "SynthesizeMouseEvent", eventData); - } else { - logParent("DragEnd: Target browser rect is zero, cannot send mouseup."); - } - } else { - logParent("DragEnd: Called without event, mouseup not synthesized."); - } - } - } - isSynthesizingDrag = false; - dragInitialModel.targetBrowserDuringDrag = null; - mainBrowserWindow.removeEventListener('mousemove', handleSyntheticDrag, true); - mainBrowserWindow.removeEventListener('mouseup', handleSyntheticDragEnd, true); - } - - mainBrowserWindow.addEventListener('wheel', (event) => { - const activeBrowser = gBrowser.selectedBrowser; - if (!activeBrowser) return; - const windowWidth = mainBrowserWindow.innerWidth; - const activeBrowserRect = activeBrowser.getBoundingClientRect(); - const isActiveBrowserRightmost = (windowWidth - activeBrowserRect.right) < RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX; - const isInFarRightWindowEdgeGap = event.clientX > (windowWidth - EDGE_INTERACTION_WIDTH_PX) && event.clientX < windowWidth; - if (!isActiveBrowserRightmost || !isInFarRightWindowEdgeGap) return; - const targetBrowser = activeBrowser; - if (!targetBrowser.messageManager) { logParent("Wheel: No messageManager."); return; } - loadFrameScriptForBrowser(targetBrowser); - event.preventDefault(); event.stopPropagation(); - const wheelData = { - deltaX: event.deltaX, deltaY: event.deltaY, deltaZ: event.deltaZ, deltaMode: event.deltaMode, - ctrlKey: event.ctrlKey, altKey: event.altKey, shiftKey: event.shiftKey, metaKey: event.metaKey, - // For wheel, we also need clientX/Y for the frame script to target the event - clientX: Math.max(0, Math.floor(activeBrowserRect.width - SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE)), - clientY: Math.max(0, Math.min(Math.floor(event.clientY - activeBrowserRect.top), Math.floor(activeBrowserRect.height - 1))) - }; - logParent(`Wheel: Sending DispatchWheel to ${targetBrowser.currentURI?.spec} with deltaX:${wheelData.deltaX}, deltaY:${wheelData.deltaY}, clientX:${wheelData.clientX}, clientY:${wheelData.clientY}`); - targetBrowser.messageManager.sendAsyncMessage(MESSAGE_PREFIX + "DispatchWheel", { wheelData }); - }, { capture: true, passive: false }); - - if (document.readyState === "complete" || document.readyState === "interactive") { - initFrameScripts(); - } else { - mainBrowserWindow.addEventListener("load", initFrameScripts, { once: true }); - } - logParent("Edge Scroll Handler Initialized (Synthesizing Mouse Events for Drag)"); -})(); \ No newline at end of file diff --git a/src/zen/common/ZenStartup.mjs b/src/zen/common/ZenStartup.mjs index fd3c3a9d29..731031940a 100644 --- a/src/zen/common/ZenStartup.mjs +++ b/src/zen/common/ZenStartup.mjs @@ -44,19 +44,6 @@ gZenVerticalTabsManager.init(); gZenUIManager.init(); - // --- Load EdgeScrollHandler.js --- - try { - // Assuming EdgeScrollHandler.js is packaged to be accessible via this URI - // and your chrome.manifest maps chrome://zen/content/ui/... correctly. - const edgeScrollHandlerURL = "chrome://browser/content/ZenEdgeScrollHandler.js"; - Services.scriptloader.loadSubScript(edgeScrollHandlerURL, window, "UTF-8"); - console.log("ZenStartup: EdgeScrollHandler.js loaded successfully."); - } catch (e) { - console.error("ZenStartup: Failed to load EdgeScrollHandler.js:", e); - dump("ZenStartup: Failed to load EdgeScrollHandler.js: " + e + "\n" + (e.stack || "") + "\n"); - } - // --- End Load EdgeScrollHandler.js --- - this._checkForWelcomePage(); document.l10n.setAttributes( diff --git a/src/zen/edgescroll/ZenEdgeScrollManager.mjs b/src/zen/edgescroll/ZenEdgeScrollManager.mjs new file mode 100644 index 0000000000..29c0553f71 --- /dev/null +++ b/src/zen/edgescroll/ZenEdgeScrollManager.mjs @@ -0,0 +1,318 @@ +/* global window, document, gBrowser, Services, ChromeUtils */ // Assuming gBrowser etc. are available + +const EDGE_INTERACTION_WIDTH_PX = 20; +const RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX = 25; +const SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE = 2; +const ACTOR_NAME = "ZenEdgeScroll"; // Name used for actor registration + +function logManager(message) { + dump("ZenEdgeScrollManager: " + message + "\n"); +} + +class ZenEdgeScrollManager { + constructor(windowGlobal) { + this.window = windowGlobal; + this.gBrowser = this.window.gBrowser; + this.isSynthesizingDrag = false; + this.dragInitialModel = { + targetBrowserDuringDrag: null, + targetBrowsingContextDuringDrag: null, + }; + + this._boundHandleMouseDown = this.handleMouseDown.bind(this); + this._boundHandleSyntheticDrag = this.handleSyntheticDrag.bind(this); + this._boundHandleSyntheticDragEnd = this.handleSyntheticDragEnd.bind(this); + this._boundHandleWheel = this.handleWheel.bind(this); + + // logManager("Constructor created"); + } + + init() { + if (this.window.gZenEdgeScrollManagerInitialized) { + // logManager("Already initialized for this window."); + return; + } + this.window.gZenEdgeScrollManagerInitialized = true; + this.gBrowser = this.window.gBrowser; + if (!this.gBrowser) { + logManager("No gBrowser found. Cannot initialize."); + return; + } + + this.window.addEventListener('mousedown', this._boundHandleMouseDown, true); + this.window.addEventListener('wheel', this._boundHandleWheel, { capture: true, passive: false }); + + logManager("Initialized and event listeners added."); + } + + destroy() { + this.window.removeEventListener('mousedown', this._boundHandleMouseDown, true); + this.window.removeEventListener('mousemove', this._boundHandleSyntheticDrag, true); + this.window.removeEventListener('mouseup', this._boundHandleSyntheticDragEnd, true); + this.window.removeEventListener('wheel', this._boundHandleWheel, true); + this.window.gZenEdgeScrollManagerInitialized = false; + logManager("Destroyed, listeners removed."); + } + + _getParentActor() { + logManager(`_getParentActor: Called. this.window.location?.href is: ${this.window.location?.href}`); + logManager(`_getParentActor: Does this.window have windowGlobalChild? ${!!this.gBrowser.selectedBrowser.browsingContext.currentWindowGlobal}`); + if (!this.gBrowser.selectedBrowser.browsingContext.currentWindowGlobal) { + logManager("_getParentActor: No windowGlobalChild on this.window. Returning null."); + return null; + } + try { + const actor = this.gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor(ACTOR_NAME); + logManager(`_getParentActor: getActor('${ACTOR_NAME}') returned: ${actor}`); // Original log + if (actor) { + } + return actor; + } catch (e) { + logManager(`_getParentActor: Error in getActor('${ACTOR_NAME}'): ${e}`); + return null; + } + } + + getGapZoneInfo(event) { + const windowWidth = this.window.innerWidth; + const eventClientX = event.clientX; + const eventClientY = event.clientY; + const isInFarRightWindowEdgeGap = eventClientX > (windowWidth - EDGE_INTERACTION_WIDTH_PX) && eventClientX < windowWidth; + if (!isInFarRightWindowEdgeGap) return { isInGap: false, targetBrowser: null, browserRect: null }; + + let potentialTargetBrowser = null; + let potentialTargetBrowserRect = null; + if (this.gBrowser && this.gBrowser.browsers) { + for (const browser of this.gBrowser.browsers) { + // Ensure browser is suitable (visible, primary, not transparent new tab) + if (browser.hidden || browser.getAttribute("transparent") === "true" || !browser.getAttribute("primary")) { + continue; + } + // Add any other Zen-specific checks for "active/rendered" browser if needed + // For example, checking against gZenViewSplitter.isActiveBrowser(browser) if such a method exists + + const browserRect = browser.getBoundingClientRect(); + if (browserRect.width === 0 || browserRect.height === 0) continue; + + const isBrowserAtRightEdge = (windowWidth - browserRect.right) < RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX && browserRect.right > (windowWidth - EDGE_INTERACTION_WIDTH_PX - RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX); + const isEventYWithinBrowser = eventClientY >= browserRect.top && eventClientY <= browserRect.bottom; + + if (isBrowserAtRightEdge && isEventYWithinBrowser) { + potentialTargetBrowser = browser; + potentialTargetBrowserRect = browserRect; + break; + } + } + } + if (potentialTargetBrowser) { + return { isInGap: true, targetBrowser: potentialTargetBrowser, browserRect: potentialTargetBrowserRect }; + } + return { isInGap: true, targetBrowser: null, browserRect: null }; + } + + createSyntheticEventData(originalEvent, targetBrowserRect, eventType) { + const clientXInContent = Math.max(0, Math.floor(targetBrowserRect.width - SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE)); + const clientYInContent = Math.max(0, Math.min(Math.floor(originalEvent.clientY - targetBrowserRect.top), Math.floor(targetBrowserRect.height - 1))); + const screenX = Math.floor(this.window.screenX + targetBrowserRect.left + clientXInContent); + const screenY = Math.floor(this.window.screenY + targetBrowserRect.top + clientYInContent); + + return { + type: eventType, clientX: clientXInContent, clientY: clientYInContent, + screenX: screenX, screenY: screenY, button: originalEvent.button, + buttons: (eventType === 'mousemove' || eventType === 'mousedown') ? 1 : 0, + ctrlKey: originalEvent.ctrlKey, altKey: originalEvent.altKey, + shiftKey: originalEvent.shiftKey, metaKey: originalEvent.metaKey, + }; + } + + handleMouseDown(event) { + if (event.button !== 0) return; + const gapInfo = this.getGapZoneInfo(event); + + if (!gapInfo.isInGap || !gapInfo.targetBrowser) { + if (gapInfo.isInGap) { /* logManager("Mousedown: In gap, but no specific adjacent browser."); */ } + return; + } + let targetBrowser = gapInfo.targetBrowser; + + // Logic to switch tab if a non-selected browser is targeted + if (targetBrowser !== this.gBrowser.selectedBrowser) { + const tabToSelect = this.gBrowser.getTabForBrowser(targetBrowser); + if (tabToSelect && this.gBrowser.selectedTab !== tabToSelect) { + this.gBrowser.selectedTab = tabToSelect; + targetBrowser = this.gBrowser.selectedBrowser; // Re-assign after selection + if (targetBrowser) gapInfo.browserRect = targetBrowser.getBoundingClientRect(); + else { logManager("Mousedown: Target browser null after tab selection."); return; } + } + } + + const parentActor = this._getParentActor(); + + // Add a check before calling the method + if (!parentActor) { + logManager("Mousedown: parentActor is null. Bailing out."); + return; + } + if (typeof parentActor.sendEventToChild !== 'function') { + logManager(`Mousedown: parentActor.sendEventToChild is NOT a function. Actual type: ${typeof parentActor.sendEventToChild}. Actor constructor: ${parentActor?.constructor?.name}. Bailing out.`); + return; + } + if (!targetBrowser?.browsingContext) { // Check this *after* confirming parentActor is valid + logManager("Mousedown: No targetBrowser.browsingContext. Bailing out. Target: " + targetBrowser?.currentURI?.spec); return; + } + + event.preventDefault(); + this.isSynthesizingDrag = true; + this.dragInitialModel.targetBrowserDuringDrag = targetBrowser; + this.dragInitialModel.targetBrowsingContextDuringDrag = targetBrowser.browsingContext; + + const eventData = this.createSyntheticEventData(event, gapInfo.browserRect, 'mousedown'); + // logManager(`Mousedown: Sending SynthesizeMouseEvent to ${targetBrowser.currentURI?.spec}`); + parentActor.sendEventToChild(targetBrowser.browsingContext, "ZenEdgeScroll:SynthesizeMouseEvent", eventData); + + this.window.addEventListener('mousemove', this._boundHandleSyntheticDrag, true); + this.window.addEventListener('mouseup', this._boundHandleSyntheticDragEnd, true); + } + + handleSyntheticDrag(event) { + if (!this.isSynthesizingDrag || !this.dragInitialModel.targetBrowsingContextDuringDrag) return; + + const targetBrowser = this.dragInitialModel.targetBrowserDuringDrag; + const targetBrowsingContext = this.dragInitialModel.targetBrowsingContextDuringDrag; + + if (this.gBrowser.selectedBrowser !== targetBrowser) { + // logManager("Drag: Target browser changed. Ending drag."); + this.handleSyntheticDragEnd(event); return; + } + + const parentActor = this._getParentActor(); + if (!parentActor) { + logManager("Drag: No parentActor. Ending drag."); + this.handleSyntheticDragEnd(event); return; + } + + event.preventDefault(); event.stopPropagation(); + const currentTargetBrowserRect = targetBrowser.getBoundingClientRect(); + if (currentTargetBrowserRect.width === 0 || currentTargetBrowserRect.height === 0) { + // logManager("Drag: Target browser rect is zero. Ending drag."); + this.handleSyntheticDragEnd(event); return; + } + const eventData = this.createSyntheticEventData(event, currentTargetBrowserRect, 'mousemove'); + parentActor.sendEventToChild(targetBrowsingContext, "ZenEdgeScroll:SynthesizeMouseEvent", eventData); + } + + handleSyntheticDragEnd(event) { + if (this.isSynthesizingDrag && this.dragInitialModel.targetBrowsingContextDuringDrag) { + const targetBrowser = this.dragInitialModel.targetBrowserDuringDrag; + const targetBrowsingContext = this.dragInitialModel.targetBrowsingContextDuringDrag; + const parentActor = this._getParentActor(); + + if (parentActor && event) { // If called by an event + event.preventDefault(); event.stopPropagation(); + const currentTargetBrowserRect = targetBrowser.getBoundingClientRect(); + if (currentTargetBrowserRect.width > 0 && currentTargetBrowserRect.height > 0) { + const eventData = this.createSyntheticEventData(event, currentTargetBrowserRect, 'mouseup'); + // logManager(`DragEnd: Sending SynthesizeMouseEvent to ${targetBrowser?.currentURI?.spec}`); + parentActor.sendEventToChild(targetBrowsingContext, "ZenEdgeScroll:SynthesizeMouseEvent", eventData); + } else { + // logManager("DragEnd: Target browser rect is zero, cannot send mouseup."); + } + } else if (parentActor && !event) { // Called without event (e.g. drag cancelled) + // logManager("DragEnd: Called without event, mouseup not synthesized via event data."); + // Optionally send a generic mouseup if needed, or just clean up. + } + } + this.isSynthesizingDrag = false; + this.dragInitialModel.targetBrowserDuringDrag = null; + this.dragInitialModel.targetBrowsingContextDuringDrag = null; + this.window.removeEventListener('mousemove', this._boundHandleSyntheticDrag, true); + this.window.removeEventListener('mouseup', this._boundHandleSyntheticDragEnd, true); + } + + handleWheel(event) { + const activeBrowser = this.gBrowser.selectedBrowser; + if (!activeBrowser) return; + + const windowWidth = this.window.innerWidth; + const activeBrowserRect = activeBrowser.getBoundingClientRect(); + const isActiveBrowserRightmost = (windowWidth - activeBrowserRect.right) < RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX; + const isInFarRightWindowEdgeGap = event.clientX > (windowWidth - EDGE_INTERACTION_WIDTH_PX) && event.clientX < windowWidth; + + if (!isActiveBrowserRightmost || !isInFarRightWindowEdgeGap) return; + + const targetBrowser = activeBrowser; + const parentActor = this._getParentActor(); + + if (!parentActor || !targetBrowser.browsingContext) { + logManager("Wheel: No parentActor or browsingContext for target browser: " + targetBrowser.currentURI?.spec); return; + } + + event.preventDefault(); event.stopPropagation(); + const wheelData = { + deltaX: event.deltaX, deltaY: event.deltaY, deltaZ: event.deltaZ, deltaMode: event.deltaMode, + ctrlKey: event.ctrlKey, altKey: event.altKey, shiftKey: event.shiftKey, metaKey: event.metaKey, + clientX: Math.max(0, Math.floor(activeBrowserRect.width - SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE)), + clientY: Math.max(0, Math.min(Math.floor(event.clientY - activeBrowserRect.top), Math.floor(activeBrowserRect.height - 1))) + }; + // logManager(`Wheel: Sending DispatchWheel to ${targetBrowser.currentURI?.spec}`); + parentActor.sendEventToChild(targetBrowser.browsingContext, "ZenEdgeScroll:DispatchWheel", { wheelData }); + } +} + +// Initialization and Actor Registration +// This should be called once per window, typically during browser startup. +// Adapt this to how Zen Desktop initializes its managers and registers actors. + +(function() { + if (window.gZenEdgeScrollManagerInstance) { + return; + } + + // Actor Registration (must happen before manager instantiation if manager relies on actors being ready) + // This is modeled after ZenGlanceManager's registerWindowActors + function registerEdgeScrollActors() { + const actorConfig = { + parent: { + esModuleURI: 'chrome://browser/content/zen-components/actors/ZenEdgeScrollParent.sys.mjs', + }, + child: { + esModuleURI: 'chrome://browser/content/zen-components/actors/ZenEdgeScrollChild.sys.mjs', + }, + allFrames: true, // Child actor should be available in all frames + matches: [ + '*://*/*', // For general content pages + 'chrome://*/*' // Explicitly allow all chrome URIs + // OR even more specific for testing: + // 'chrome://browser/content/browser.xhtml' + ], + includeChrome: true, // <--- ENSURE THIS LINE IS PRESENT AND SET TO TRUE + }; + + if (window.gZenActorsManager && typeof window.gZenActorsManager.addJSWindowActor === 'function') { + window.gZenActorsManager.addJSWindowActor(ACTOR_NAME, actorConfig); + logManager(`${ACTOR_NAME} actors registered via gZenActorsManager.`); + } else { + try { + ChromeUtils.registerWindowActor(ACTOR_NAME, { // Name must match ACTOR_NAME + parent: { moduleURI: actorConfig.parent.esModuleURI }, + child: { moduleURI: actorConfig.child.esModuleURI }, + matches: actorConfig.matches, + allFrames: actorConfig.allFrames, + }); + logManager(`${ACTOR_NAME} actors registered via ChromeUtils.registerWindowActor.`); + } catch (e) { + console.error(`Failed to register ${ACTOR_NAME} actors:`, e); + logManager(`Failed to register ${ACTOR_NAME} actors: ${e}`); + } + } + } + + registerEdgeScrollActors(); + + window.gZenEdgeScrollManagerInstance = new ZenEdgeScrollManager(window); + if (document.readyState === "complete" || document.readyState === "interactive") { + window.gZenEdgeScrollManagerInstance.init(); + } else { + window.addEventListener("load", () => window.gZenEdgeScrollManagerInstance.init(), { once: true }); + } +})(); \ No newline at end of file diff --git a/src/zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs b/src/zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs new file mode 100644 index 0000000000..2f6869f1fe --- /dev/null +++ b/src/zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs @@ -0,0 +1,107 @@ +/* eslint-env mozilla/frame-script */ +/* global content, Components */ // For linter + +const { utils: Cu, interfaces: Ci } = Components; + +function logChild(message) { + dump("ZenEdgeScrollChild: " + message + "\n"); + // console.log("ZenEdgeScrollChild: " + message + "\n"); // Optional: for browser console +} + +export class ZenEdgeScrollChild extends JSWindowActorChild { + constructor() { + super(); + // logChild("Constructor. Initial content URL: " + (this.contentWindow?.document?.location?.href || "unknown")); + } + + receiveMessage(message) { + logChild(`Received message in child: ${message.name} for URL: ${this.contentWindow?.document?.location?.href || "unknown"}`); + switch (message.name) { + case "ZenEdgeScroll:SynthesizeMouseEvent": + this.handleSynthesizeMouseEvent(message.data); + break; + case "ZenEdgeScroll:DispatchWheel": + this.handleDispatchWheel(message.data); + break; + default: + logChild(`Unknown message received: ${message.name}`); + } + } + + handleSynthesizeMouseEvent(data) { + if (!data || !data.type) { + logChild("SynthesizeMouseEvent: Invalid data received."); + return; + } + + const contentWin = this.contentWindow; + if (!contentWin || !contentWin.windowUtils) { + logChild("SynthesizeMouseEvent: content.windowUtils is not available. URL: " + (contentWin?.document?.location?.href || "unknown")); + return; + } + + let modifiers = 0; + if (data.altKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_ALT; + if (data.ctrlKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_CONTROL; + if (data.metaKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_META; + if (data.shiftKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_SHIFT; + + if (data.type === "mousemove" && data.buttons === 1) { + modifiers |= Ci.nsIDOMWindowUtils.BUTTON_PRIMARY_ACTION; + } + + let clickCount = 0; + if (data.type === "mousedown" || data.type === "mouseup") { + clickCount = 1; + } + + try { + // logChild(`SynthesizeMouseEvent: Dispatching ${data.type} to ${contentWin.document.location.href} at X:${data.clientX}, Y:${data.clientY}`); + contentWin.windowUtils.sendMouseEvent( + data.type, data.clientX, data.clientY, data.button, + clickCount, modifiers, false, 0.5, + Ci.nsIDOMWindowUtils.INPUT_SOURCE_MOUSE, false + ); + } catch (e) { + logChild(`Error dispatching trusted synthetic ${data.type} event: ${e} - ${e.stack}. URL: ` + (contentWin?.document?.location?.href || "unknown")); + } + } + + handleDispatchWheel({ wheelData }) { + if (!wheelData) { + logChild("DispatchWheel: No wheelData received."); + return; + } + const contentWin = this.contentWindow; + if (!contentWin || !contentWin.windowUtils) { + logChild("DispatchWheel: content.windowUtils is not available. URL: " + (contentWin?.document?.location?.href || "unknown")); + return; + } + const doc = contentWin.document; + + + const clientX = typeof wheelData.clientX === 'number' ? wheelData.clientX : (doc.documentElement.clientWidth / 2); + const clientY = typeof wheelData.clientY === 'number' ? wheelData.clientY : (doc.documentElement.clientHeight / 2); + + let modifiers = 0; + if (wheelData.altKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_ALT; + if (wheelData.ctrlKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_CONTROL; + if (wheelData.metaKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_META; + if (wheelData.shiftKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_SHIFT; + + try { + // logChild(`DispatchWheel: Dispatching to ${contentWin.document.location.href} at X:${clientX}, Y:${clientY}`); + contentWin.windowUtils.sendWheelEvent( + clientX, clientY, wheelData.deltaX, wheelData.deltaY, wheelData.deltaZ, + wheelData.deltaMode, modifiers, 0, 0, true, false, false, false, false + ); + } catch (e) { + logChild(`Error dispatching trusted wheel event: ${e} - ${e.stack}. URL: ` + (contentWin?.document?.location?.href || "unknown")); + } + } + + destroy() { + // logChild("Destroying ZenEdgeScrollChild for " + (this.contentWindow?.document?.location?.href || "unknown")); + super.destroy(); + } +} \ No newline at end of file diff --git a/src/zen/edgescroll/actors/ZenEdgeScrollParent.sys.mjs b/src/zen/edgescroll/actors/ZenEdgeScrollParent.sys.mjs new file mode 100644 index 0000000000..444772bc30 --- /dev/null +++ b/src/zen/edgescroll/actors/ZenEdgeScrollParent.sys.mjs @@ -0,0 +1,36 @@ +/* global Services */ // For linter + +console.log("ZenEdgeScrollParent: " + "alive" + "\n"); + +function logParentActor(message) { + dump("ZenEdgeScrollParentActor: " + message + "\n"); +} + +export class ZenEdgeScrollParent extends JSWindowActorParent { + constructor() { + super(); + // logParentActor("Constructor"); + } + + // This actor primarily sends messages to its children. + // It might receive messages if a child needs to query the parent for info. + async receiveMessage(message) { + logParentActor(`Parent received message: ${message.name} from child in ${message.browsingContext?.currentWindowGlobal?.documentURI?.spec}`); + // Handle any messages from child if needed in the future + } + + // Called by ZenEdgeScrollManager to send a message to a specific child actor + sendEventToChild(browsingContext, messageName, eventData) { + if (!browsingContext) { + logParentActor(`sendEventToChild: No browsingContext provided for ${messageName}.`); + return; + } + logParentActor(`Parent sending ${messageName} to child in context: ${browsingContext.currentWindowGlobal?.documentURI?.spec}`); + this.sendAsyncMessage(messageName, eventData); + } + + destroy() { + // logParentActor("Destroying ZenEdgeScrollParent"); + super.destroy(); + } +} \ No newline at end of file diff --git a/src/zen/edgescroll/moz.build b/src/zen/edgescroll/moz.build new file mode 100644 index 0000000000..cf58b8671e --- /dev/null +++ b/src/zen/edgescroll/moz.build @@ -0,0 +1,10 @@ +# +# 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/. + + +FINAL_TARGET_FILES.actors += [ + "actors/ZenEdgeScrollChild.sys.mjs", + "actors/ZenEdgeScrollParent.sys.mjs", +] diff --git a/src/zen/moz.build b/src/zen/moz.build index b17cc79159..b28a881a50 100644 --- a/src/zen/moz.build +++ b/src/zen/moz.build @@ -8,4 +8,5 @@ DIRS += [ "mods", "tests", "toolkit", + "edgescroll", ] From 67742d93e28b3dc3e6ac97f34064e7184d6578b6 Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Wed, 21 May 2025 20:18:30 +0800 Subject: [PATCH 08/34] Refine code --- src/zen/edgescroll/ZenEdgeScrollManager.mjs | 474 ++++++++++-------- .../actors/ZenEdgeScrollChild.sys.mjs | 6 +- .../actors/ZenEdgeScrollParent.sys.mjs | 6 +- 3 files changed, 261 insertions(+), 225 deletions(-) diff --git a/src/zen/edgescroll/ZenEdgeScrollManager.mjs b/src/zen/edgescroll/ZenEdgeScrollManager.mjs index 29c0553f71..fe9574d6af 100644 --- a/src/zen/edgescroll/ZenEdgeScrollManager.mjs +++ b/src/zen/edgescroll/ZenEdgeScrollManager.mjs @@ -1,273 +1,313 @@ /* global window, document, gBrowser, Services, ChromeUtils */ // Assuming gBrowser etc. are available +{ + const EDGE_INTERACTION_WIDTH_PX = Services.prefs.getIntPref("zen.theme.border-radius", 8); + const SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE = 1; + const ACTOR_NAME = "ZenEdgeScroll"; // Name used for actor registration -const EDGE_INTERACTION_WIDTH_PX = 20; -const RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX = 25; -const SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE = 2; -const ACTOR_NAME = "ZenEdgeScroll"; // Name used for actor registration - -function logManager(message) { - dump("ZenEdgeScrollManager: " + message + "\n"); -} - -class ZenEdgeScrollManager { - constructor(windowGlobal) { - this.window = windowGlobal; - this.gBrowser = this.window.gBrowser; - this.isSynthesizingDrag = false; - this.dragInitialModel = { - targetBrowserDuringDrag: null, - targetBrowsingContextDuringDrag: null, - }; - - this._boundHandleMouseDown = this.handleMouseDown.bind(this); - this._boundHandleSyntheticDrag = this.handleSyntheticDrag.bind(this); - this._boundHandleSyntheticDragEnd = this.handleSyntheticDragEnd.bind(this); - this._boundHandleWheel = this.handleWheel.bind(this); - - // logManager("Constructor created"); + function logManager(message) { + dump("ZenEdgeScrollManager: " + message + "\n"); } - init() { - if (this.window.gZenEdgeScrollManagerInitialized) { - // logManager("Already initialized for this window."); - return; + class ZenEdgeScrollManager { + constructor(windowGlobal) { + this.window = windowGlobal; + this.gBrowser = this.window.gBrowser; + this.isSynthesizingDrag = false; + this.dragInitialModel = { + targetBrowserDuringDrag: null, + targetBrowsingContextDuringDrag: null, + }; + this.edgeScrollTriggerDiv = null; // Added for the trigger div + + this._boundHandleMouseDown = this.handleMouseDown.bind(this); + this._boundHandleSyntheticDrag = this.handleSyntheticDrag.bind(this); + this._boundHandleSyntheticDragEnd = this.handleSyntheticDragEnd.bind(this); + this._boundHandleWheel = this.handleWheel.bind(this); + + // logManager("Constructor created"); } - this.window.gZenEdgeScrollManagerInitialized = true; - this.gBrowser = this.window.gBrowser; - if (!this.gBrowser) { + + init() { + if (this.window.gZenEdgeScrollManagerInitialized) { + // logManager("Already initialized for this window."); + return; + } + this.window.gZenEdgeScrollManagerInitialized = true; + this.gBrowser = this.window.gBrowser; + if (!this.gBrowser) { logManager("No gBrowser found. Cannot initialize."); return; - } - - this.window.addEventListener('mousedown', this._boundHandleMouseDown, true); - this.window.addEventListener('wheel', this._boundHandleWheel, { capture: true, passive: false }); - - logManager("Initialized and event listeners added."); - } - - destroy() { - this.window.removeEventListener('mousedown', this._boundHandleMouseDown, true); - this.window.removeEventListener('mousemove', this._boundHandleSyntheticDrag, true); - this.window.removeEventListener('mouseup', this._boundHandleSyntheticDragEnd, true); - this.window.removeEventListener('wheel', this._boundHandleWheel, true); - this.window.gZenEdgeScrollManagerInitialized = false; - logManager("Destroyed, listeners removed."); - } + } - _getParentActor() { - logManager(`_getParentActor: Called. this.window.location?.href is: ${this.window.location?.href}`); - logManager(`_getParentActor: Does this.window have windowGlobalChild? ${!!this.gBrowser.selectedBrowser.browsingContext.currentWindowGlobal}`); - if (!this.gBrowser.selectedBrowser.browsingContext.currentWindowGlobal) { - logManager("_getParentActor: No windowGlobalChild on this.window. Returning null."); - return null; + // Create and append the edge scroll trigger div + this.edgeScrollTriggerDiv = this.window.document.createElement("div"); + this.edgeScrollTriggerDiv.id = "zen-edge-scroll-trigger"; + Object.assign(this.edgeScrollTriggerDiv.style, { + position: "fixed", + top: "0px", + right: "0px", + width: `${EDGE_INTERACTION_WIDTH_PX}px`, + height: "100%", + zIndex: "2147483647", // Max z-index + userSelect: "none", + // backgroundColor: "rgba(255,0,0,0.1)", // For debugging visibility + }); + this.window.document.documentElement.appendChild(this.edgeScrollTriggerDiv); + + this.edgeScrollTriggerDiv.addEventListener('mousedown', this._boundHandleMouseDown, true); + this.edgeScrollTriggerDiv.addEventListener('wheel', this._boundHandleWheel, { capture: true, passive: false }); + + logManager("Initialized, edgeScrollTriggerDiv created, and event listeners added."); } - try { - const actor = this.gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor(ACTOR_NAME); - logManager(`_getParentActor: getActor('${ACTOR_NAME}') returned: ${actor}`); // Original log - if (actor) { + + destroy() { + if (this.edgeScrollTriggerDiv) { + this.edgeScrollTriggerDiv.removeEventListener('mousedown', this._boundHandleMouseDown, true); + this.edgeScrollTriggerDiv.removeEventListener('wheel', this._boundHandleWheel, true); + if (this.edgeScrollTriggerDiv.parentNode) { + this.edgeScrollTriggerDiv.parentNode.removeChild(this.edgeScrollTriggerDiv); + } + this.edgeScrollTriggerDiv = null; } - return actor; - } catch (e) { - logManager(`_getParentActor: Error in getActor('${ACTOR_NAME}'): ${e}`); - return null; + // Listeners for drag are on the window, keep them if drag is active, but they are added/removed dynamically + this.edgeScrollTriggerDiv.removeEventListener('mousemove', this._boundHandleSyntheticDrag, true); + this.edgeScrollTriggerDiv.removeEventListener('mouseup', this._boundHandleSyntheticDragEnd, true); + this.window.gZenEdgeScrollManagerInitialized = false; + logManager("Destroyed, listeners removed, edgeScrollTriggerDiv removed."); } - } - getGapZoneInfo(event) { - const windowWidth = this.window.innerWidth; - const eventClientX = event.clientX; - const eventClientY = event.clientY; - const isInFarRightWindowEdgeGap = eventClientX > (windowWidth - EDGE_INTERACTION_WIDTH_PX) && eventClientX < windowWidth; - if (!isInFarRightWindowEdgeGap) return { isInGap: false, targetBrowser: null, browserRect: null }; + _getParentActor() { + if (!this.gBrowser.selectedBrowser.browsingContext.currentWindowGlobal) { + logManager("_getParentActor: No windowGlobalChild on this.window. Returning null."); + return null; + } + try { + const actor = this.gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor(ACTOR_NAME); + logManager(`_getParentActor: getActor('${ACTOR_NAME}') returned: ${actor}`); // Original log + if (actor) { + } + return actor; + } catch (e) { + logManager(`_getParentActor: Error in getActor('${ACTOR_NAME}'): ${e}`); + return null; + } + } - let potentialTargetBrowser = null; - let potentialTargetBrowserRect = null; - if (this.gBrowser && this.gBrowser.browsers) { + getGapZoneInfo(event) { + const windowWidth = this.window.innerWidth; + // const eventClientX = event.clientX; // event.clientX is on the trigger div + const eventClientY = event.clientY; + // const isInFarRightWindowEdgeGap = eventClientX > (windowWidth - EDGE_INTERACTION_WIDTH_PX) && event.clientX < windowWidth; + // If event is from edgeScrollTriggerDiv, it's always in the "gap". + // We just need to find the browser adjacent to this event. + + let potentialTargetBrowser = null; + let potentialTargetBrowserRect = null; + if (this.gBrowser && this.gBrowser.browsers) { for (const browser of this.gBrowser.browsers) { - // Ensure browser is suitable (visible, primary, not transparent new tab) - if (browser.hidden || browser.getAttribute("transparent") === "true" || !browser.getAttribute("primary")) { - continue; - } - // Add any other Zen-specific checks for "active/rendered" browser if needed - // For example, checking against gZenViewSplitter.isActiveBrowser(browser) if such a method exists - - const browserRect = browser.getBoundingClientRect(); - if (browserRect.width === 0 || browserRect.height === 0) continue; - - const isBrowserAtRightEdge = (windowWidth - browserRect.right) < RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX && browserRect.right > (windowWidth - EDGE_INTERACTION_WIDTH_PX - RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX); - const isEventYWithinBrowser = eventClientY >= browserRect.top && eventClientY <= browserRect.bottom; - - if (isBrowserAtRightEdge && isEventYWithinBrowser) { - potentialTargetBrowser = browser; - potentialTargetBrowserRect = browserRect; - break; - } + if (browser.hidden || browser.getAttribute("transparent") === "true" || !browser.getAttribute("primary")) { + continue; + } + + const browserRect = browser.getBoundingClientRect(); + if (browserRect.width === 0 || browserRect.height === 0) continue; + + // Check if the browser's right edge is very close to the window's right edge + const isBrowserAtRightEdge = (windowWidth - browserRect.right) <= EDGE_INTERACTION_WIDTH_PX + 1; + const isEventYWithinBrowser = eventClientY >= browserRect.top && eventClientY <= browserRect.bottom; + + if (isBrowserAtRightEdge && isEventYWithinBrowser) { + potentialTargetBrowser = browser; + potentialTargetBrowserRect = browserRect; + break; // Found the target browser + } } - } - if (potentialTargetBrowser) { + } + // The event is on the trigger div, so it is "in gap". We return the browser found. return { isInGap: true, targetBrowser: potentialTargetBrowser, browserRect: potentialTargetBrowserRect }; } - return { isInGap: true, targetBrowser: null, browserRect: null }; - } - createSyntheticEventData(originalEvent, targetBrowserRect, eventType) { - const clientXInContent = Math.max(0, Math.floor(targetBrowserRect.width - SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE)); - const clientYInContent = Math.max(0, Math.min(Math.floor(originalEvent.clientY - targetBrowserRect.top), Math.floor(targetBrowserRect.height - 1))); - const screenX = Math.floor(this.window.screenX + targetBrowserRect.left + clientXInContent); - const screenY = Math.floor(this.window.screenY + targetBrowserRect.top + clientYInContent); - - return { - type: eventType, clientX: clientXInContent, clientY: clientYInContent, - screenX: screenX, screenY: screenY, button: originalEvent.button, - buttons: (eventType === 'mousemove' || eventType === 'mousedown') ? 1 : 0, - ctrlKey: originalEvent.ctrlKey, altKey: originalEvent.altKey, - shiftKey: originalEvent.shiftKey, metaKey: originalEvent.metaKey, - }; - } + createSyntheticEventData(originalEvent, targetBrowserRect, eventType) { + const clientXInContent = Math.max(0, Math.floor(targetBrowserRect.width - SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE)); + const clientYInContent = Math.max(0, Math.min(Math.floor(originalEvent.clientY - targetBrowserRect.top), Math.floor(targetBrowserRect.height - 1))); + const screenX = Math.floor(this.window.screenX + targetBrowserRect.left + clientXInContent); + const screenY = Math.floor(this.window.screenY + targetBrowserRect.top + clientYInContent); + + return { + type: eventType, clientX: clientXInContent, clientY: clientYInContent, + screenX: screenX, screenY: screenY, button: originalEvent.button, + buttons: (eventType === 'mousemove' || eventType === 'mousedown') ? 1 : 0, + ctrlKey: originalEvent.ctrlKey, altKey: originalEvent.altKey, + shiftKey: originalEvent.shiftKey, metaKey: originalEvent.metaKey, + }; + } - handleMouseDown(event) { - if (event.button !== 0) return; - const gapInfo = this.getGapZoneInfo(event); + handleMouseDown(event) { + if (event.button !== 0) return; + const gapInfo = this.getGapZoneInfo(event); // event is from edgeScrollTriggerDiv - if (!gapInfo.isInGap || !gapInfo.targetBrowser) { - if (gapInfo.isInGap) { /* logManager("Mousedown: In gap, but no specific adjacent browser."); */ } + if (!gapInfo.targetBrowser) { + // logManager("Mousedown: Event on trigger div, but no specific adjacent browser."); return; - } - let targetBrowser = gapInfo.targetBrowser; + } + let targetBrowser = gapInfo.targetBrowser; + let targetBrowserRect = gapInfo.browserRect; - // Logic to switch tab if a non-selected browser is targeted - if (targetBrowser !== this.gBrowser.selectedBrowser) { + // Logic to switch tab if a non-selected browser is targeted + if (targetBrowser !== this.gBrowser.selectedBrowser) { const tabToSelect = this.gBrowser.getTabForBrowser(targetBrowser); if (tabToSelect && this.gBrowser.selectedTab !== tabToSelect) { - this.gBrowser.selectedTab = tabToSelect; - targetBrowser = this.gBrowser.selectedBrowser; // Re-assign after selection - if (targetBrowser) gapInfo.browserRect = targetBrowser.getBoundingClientRect(); - else { logManager("Mousedown: Target browser null after tab selection."); return; } + this.gBrowser.selectedTab = tabToSelect; + // After tab switch, gBrowser.selectedBrowser might take a moment to update, + // or might not be the one we expect if the switch fails or is async. + // It's safer to re-get the selectedBrowser and its rect. + targetBrowser = this.gBrowser.selectedBrowser; + if (targetBrowser) { + targetBrowserRect = targetBrowser.getBoundingClientRect(); + } else { + logManager("Mousedown: Target browser null after tab selection attempt."); return; + } } - } + } + // Ensure targetBrowser and its rect are valid after potential tab switch + if (!targetBrowser || !targetBrowserRect || targetBrowserRect.width === 0 || targetBrowserRect.height === 0) { + logManager("Mousedown: Invalid targetBrowser or rect after potential tab switch. Bailing out."); + return; + } + - const parentActor = this._getParentActor(); + const parentActor = this._getParentActor(); - // Add a check before calling the method - if (!parentActor) { + // Add a check before calling the method + if (!parentActor) { logManager("Mousedown: parentActor is null. Bailing out."); return; - } - if (typeof parentActor.sendEventToChild !== 'function') { + } + if (typeof parentActor.sendEventToChild !== 'function') { logManager(`Mousedown: parentActor.sendEventToChild is NOT a function. Actual type: ${typeof parentActor.sendEventToChild}. Actor constructor: ${parentActor?.constructor?.name}. Bailing out.`); return; - } - if (!targetBrowser?.browsingContext) { // Check this *after* confirming parentActor is valid - logManager("Mousedown: No targetBrowser.browsingContext. Bailing out. Target: " + targetBrowser?.currentURI?.spec); return; - } + } + if (!targetBrowser?.browsingContext) { + logManager("Mousedown: No targetBrowser.browsingContext. Bailing out. Target: " + targetBrowser?.currentURI?.spec); return; + } - event.preventDefault(); - this.isSynthesizingDrag = true; - this.dragInitialModel.targetBrowserDuringDrag = targetBrowser; - this.dragInitialModel.targetBrowsingContextDuringDrag = targetBrowser.browsingContext; + event.preventDefault(); + this.isSynthesizingDrag = true; + this.dragInitialModel.targetBrowserDuringDrag = targetBrowser; + this.dragInitialModel.targetBrowsingContextDuringDrag = targetBrowser.browsingContext; - const eventData = this.createSyntheticEventData(event, gapInfo.browserRect, 'mousedown'); - // logManager(`Mousedown: Sending SynthesizeMouseEvent to ${targetBrowser.currentURI?.spec}`); - parentActor.sendEventToChild(targetBrowser.browsingContext, "ZenEdgeScroll:SynthesizeMouseEvent", eventData); + const eventData = this.createSyntheticEventData(event, targetBrowserRect, 'mousedown'); + // logManager(`Mousedown: Sending SynthesizeMouseEvent to ${targetBrowser.currentURI?.spec}`); + parentActor.sendEventToChild(targetBrowser.browsingContext, "ZenEdgeScroll:SynthesizeMouseEvent", eventData); - this.window.addEventListener('mousemove', this._boundHandleSyntheticDrag, true); - this.window.addEventListener('mouseup', this._boundHandleSyntheticDragEnd, true); - } + this.edgeScrollTriggerDiv.addEventListener('mousemove', this._boundHandleSyntheticDrag, true); + this.edgeScrollTriggerDiv.addEventListener('mouseup', this._boundHandleSyntheticDragEnd, true); + } - handleSyntheticDrag(event) { - if (!this.isSynthesizingDrag || !this.dragInitialModel.targetBrowsingContextDuringDrag) return; - - const targetBrowser = this.dragInitialModel.targetBrowserDuringDrag; - const targetBrowsingContext = this.dragInitialModel.targetBrowsingContextDuringDrag; + handleSyntheticDrag(event) { + if (!this.isSynthesizingDrag || !this.dragInitialModel.targetBrowsingContextDuringDrag) return; - if (this.gBrowser.selectedBrowser !== targetBrowser) { - // logManager("Drag: Target browser changed. Ending drag."); - this.handleSyntheticDragEnd(event); return; - } - - const parentActor = this._getParentActor(); - if (!parentActor) { - logManager("Drag: No parentActor. Ending drag."); - this.handleSyntheticDragEnd(event); return; - } + const targetBrowser = this.dragInitialModel.targetBrowserDuringDrag; + const targetBrowsingContext = this.dragInitialModel.targetBrowsingContextDuringDrag; + + if (this.gBrowser.selectedBrowser !== targetBrowser) { + // logManager("Drag: Target browser changed. Ending drag."); + this.handleSyntheticDragEnd(event); return; + } + + const parentActor = this._getParentActor(); + if (!parentActor) { + logManager("Drag: No parentActor. Ending drag."); + this.handleSyntheticDragEnd(event); return; + } - event.preventDefault(); event.stopPropagation(); - const currentTargetBrowserRect = targetBrowser.getBoundingClientRect(); - if (currentTargetBrowserRect.width === 0 || currentTargetBrowserRect.height === 0) { + event.preventDefault(); event.stopPropagation(); + const currentTargetBrowserRect = targetBrowser.getBoundingClientRect(); + if (currentTargetBrowserRect.width === 0 || currentTargetBrowserRect.height === 0) { // logManager("Drag: Target browser rect is zero. Ending drag."); this.handleSyntheticDragEnd(event); return; + } + const eventData = this.createSyntheticEventData(event, currentTargetBrowserRect, 'mousemove'); + parentActor.sendEventToChild(targetBrowsingContext, "ZenEdgeScroll:SynthesizeMouseEvent", eventData); } - const eventData = this.createSyntheticEventData(event, currentTargetBrowserRect, 'mousemove'); - parentActor.sendEventToChild(targetBrowsingContext, "ZenEdgeScroll:SynthesizeMouseEvent", eventData); - } - handleSyntheticDragEnd(event) { - if (this.isSynthesizingDrag && this.dragInitialModel.targetBrowsingContextDuringDrag) { + handleSyntheticDragEnd(event) { + if (this.isSynthesizingDrag && this.dragInitialModel.targetBrowsingContextDuringDrag) { const targetBrowser = this.dragInitialModel.targetBrowserDuringDrag; const targetBrowsingContext = this.dragInitialModel.targetBrowsingContextDuringDrag; const parentActor = this._getParentActor(); if (parentActor && event) { // If called by an event - event.preventDefault(); event.stopPropagation(); - const currentTargetBrowserRect = targetBrowser.getBoundingClientRect(); - if (currentTargetBrowserRect.width > 0 && currentTargetBrowserRect.height > 0) { - const eventData = this.createSyntheticEventData(event, currentTargetBrowserRect, 'mouseup'); - // logManager(`DragEnd: Sending SynthesizeMouseEvent to ${targetBrowser?.currentURI?.spec}`); - parentActor.sendEventToChild(targetBrowsingContext, "ZenEdgeScroll:SynthesizeMouseEvent", eventData); - } else { - // logManager("DragEnd: Target browser rect is zero, cannot send mouseup."); - } + event.preventDefault(); event.stopPropagation(); + const currentTargetBrowserRect = targetBrowser.getBoundingClientRect(); + if (currentTargetBrowserRect.width > 0 && currentTargetBrowserRect.height > 0) { + const eventData = this.createSyntheticEventData(event, currentTargetBrowserRect, 'mouseup'); + // logManager(`DragEnd: Sending SynthesizeMouseEvent to ${targetBrowser?.currentURI?.spec}`); + parentActor.sendEventToChild(targetBrowsingContext, "ZenEdgeScroll:SynthesizeMouseEvent", eventData); + } else { + // logManager("DragEnd: Target browser rect is zero, cannot send mouseup."); + } } else if (parentActor && !event) { // Called without event (e.g. drag cancelled) - // logManager("DragEnd: Called without event, mouseup not synthesized via event data."); - // Optionally send a generic mouseup if needed, or just clean up. + // logManager("DragEnd: Called without event, mouseup not synthesized via event data."); + // Optionally send a generic mouseup if needed, or just clean up. } + } + this.isSynthesizingDrag = false; + this.dragInitialModel.targetBrowserDuringDrag = null; + this.dragInitialModel.targetBrowsingContextDuringDrag = null; + this.edgeScrollTriggerDiv.removeEventListener('mousemove', this._boundHandleSyntheticDrag, true); + this.edgeScrollTriggerDiv.removeEventListener('mouseup', this._boundHandleSyntheticDragEnd, true); } - this.isSynthesizingDrag = false; - this.dragInitialModel.targetBrowserDuringDrag = null; - this.dragInitialModel.targetBrowsingContextDuringDrag = null; - this.window.removeEventListener('mousemove', this._boundHandleSyntheticDrag, true); - this.window.removeEventListener('mouseup', this._boundHandleSyntheticDragEnd, true); - } - handleWheel(event) { - const activeBrowser = this.gBrowser.selectedBrowser; - if (!activeBrowser) return; + handleWheel(event) { + const gapInfo = this.getGapZoneInfo(event); // event is from edgeScrollTriggerDiv - const windowWidth = this.window.innerWidth; - const activeBrowserRect = activeBrowser.getBoundingClientRect(); - const isActiveBrowserRightmost = (windowWidth - activeBrowserRect.right) < RIGHTMOST_BROWSER_PROXIMITY_THRESHOLD_PX; - const isInFarRightWindowEdgeGap = event.clientX > (windowWidth - EDGE_INTERACTION_WIDTH_PX) && event.clientX < windowWidth; + if (!gapInfo.targetBrowser) { + // logManager("Wheel: Event on trigger div, but no specific adjacent browser."); + return; + } - if (!isActiveBrowserRightmost || !isInFarRightWindowEdgeGap) return; - - const targetBrowser = activeBrowser; - const parentActor = this._getParentActor(); + const targetBrowser = gapInfo.targetBrowser; + const targetBrowserRect = gapInfo.browserRect; - if (!parentActor || !targetBrowser.browsingContext) { - logManager("Wheel: No parentActor or browsingContext for target browser: " + targetBrowser.currentURI?.spec); return; - } + // Optional: If we only want to scroll the active browser when it's the one at the edge. + // However, with the trigger div, it's more intuitive to scroll the browser that is visually there. + // if (targetBrowser !== this.gBrowser.selectedBrowser) { + // logManager("Wheel: Adjacent browser is not the active one. Ignoring wheel for now."); + // return; + // } - event.preventDefault(); event.stopPropagation(); - const wheelData = { - deltaX: event.deltaX, deltaY: event.deltaY, deltaZ: event.deltaZ, deltaMode: event.deltaMode, - ctrlKey: event.ctrlKey, altKey: event.altKey, shiftKey: event.shiftKey, metaKey: event.metaKey, - clientX: Math.max(0, Math.floor(activeBrowserRect.width - SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE)), - clientY: Math.max(0, Math.min(Math.floor(event.clientY - activeBrowserRect.top), Math.floor(activeBrowserRect.height - 1))) - }; - // logManager(`Wheel: Sending DispatchWheel to ${targetBrowser.currentURI?.spec}`); - parentActor.sendEventToChild(targetBrowser.browsingContext, "ZenEdgeScroll:DispatchWheel", { wheelData }); - } -} + const parentActor = this._getParentActor(); -// Initialization and Actor Registration -// This should be called once per window, typically during browser startup. -// Adapt this to how Zen Desktop initializes its managers and registers actors. + if (!parentActor || !targetBrowser.browsingContext) { + logManager("Wheel: No parentActor or browsingContext for target browser: " + targetBrowser.currentURI?.spec); + return; + } + if (!targetBrowserRect || targetBrowserRect.width === 0 || targetBrowserRect.height === 0) { + logManager("Wheel: Invalid targetBrowserRect for wheel event. Bailing out."); + return; + } -(function() { - if (window.gZenEdgeScrollManagerInstance) { - return; + + event.preventDefault(); event.stopPropagation(); + const wheelData = { + deltaX: event.deltaX, deltaY: event.deltaY, deltaZ: event.deltaZ, deltaMode: event.deltaMode, + ctrlKey: event.ctrlKey, altKey: event.altKey, shiftKey: event.shiftKey, metaKey: event.metaKey, + clientX: Math.max(0, Math.floor(targetBrowserRect.width - SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE)), + clientY: Math.max(0, Math.min(Math.floor(event.clientY - targetBrowserRect.top), Math.floor(targetBrowserRect.height - 1))) + }; + // logManager(`Wheel: Sending DispatchWheel to ${targetBrowser.currentURI?.spec}`); + parentActor.sendEventToChild(targetBrowser.browsingContext, "ZenEdgeScroll:DispatchWheel", { wheelData }); + } } + // Initialization and Actor Registration + // This should be called once per window, typically during browser startup. + // Adapt this to how Zen Desktop initializes its managers and registers actors. + // if (window.gZenEdgeScrollManagerInstance) { + // return; + // } + // Actor Registration (must happen before manager instantiation if manager relies on actors being ready) // This is modeled after ZenGlanceManager's registerWindowActors function registerEdgeScrollActors() { @@ -282,8 +322,6 @@ class ZenEdgeScrollManager { matches: [ '*://*/*', // For general content pages 'chrome://*/*' // Explicitly allow all chrome URIs - // OR even more specific for testing: - // 'chrome://browser/content/browser.xhtml' ], includeChrome: true, // <--- ENSURE THIS LINE IS PRESENT AND SET TO TRUE }; @@ -307,12 +345,12 @@ class ZenEdgeScrollManager { } } - registerEdgeScrollActors(); + registerEdgeScrollActors(); window.gZenEdgeScrollManagerInstance = new ZenEdgeScrollManager(window); if (document.readyState === "complete" || document.readyState === "interactive") { - window.gZenEdgeScrollManagerInstance.init(); + window.gZenEdgeScrollManagerInstance.init(); } else { - window.addEventListener("load", () => window.gZenEdgeScrollManagerInstance.init(), { once: true }); + window.addEventListener("load", () => window.gZenEdgeScrollManagerInstance.init(), { once: true }); } -})(); \ No newline at end of file +} diff --git a/src/zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs b/src/zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs index 2f6869f1fe..40b015dbb3 100644 --- a/src/zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs +++ b/src/zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs @@ -4,14 +4,12 @@ const { utils: Cu, interfaces: Ci } = Components; function logChild(message) { - dump("ZenEdgeScrollChild: " + message + "\n"); - // console.log("ZenEdgeScrollChild: " + message + "\n"); // Optional: for browser console + // dump("ZenEdgeScrollChild: " + message + "\n"); } export class ZenEdgeScrollChild extends JSWindowActorChild { constructor() { super(); - // logChild("Constructor. Initial content URL: " + (this.contentWindow?.document?.location?.href || "unknown")); } receiveMessage(message) { @@ -73,7 +71,7 @@ export class ZenEdgeScrollChild extends JSWindowActorChild { return; } const contentWin = this.contentWindow; - if (!contentWin || !contentWin.windowUtils) { + if (!contentWin || !contentWin.windowUtils) { logChild("DispatchWheel: content.windowUtils is not available. URL: " + (contentWin?.document?.location?.href || "unknown")); return; } diff --git a/src/zen/edgescroll/actors/ZenEdgeScrollParent.sys.mjs b/src/zen/edgescroll/actors/ZenEdgeScrollParent.sys.mjs index 444772bc30..da00b802f0 100644 --- a/src/zen/edgescroll/actors/ZenEdgeScrollParent.sys.mjs +++ b/src/zen/edgescroll/actors/ZenEdgeScrollParent.sys.mjs @@ -3,7 +3,7 @@ console.log("ZenEdgeScrollParent: " + "alive" + "\n"); function logParentActor(message) { - dump("ZenEdgeScrollParentActor: " + message + "\n"); + // dump("ZenEdgeScrollParentActor: " + message + "\n"); } export class ZenEdgeScrollParent extends JSWindowActorParent { @@ -22,8 +22,8 @@ export class ZenEdgeScrollParent extends JSWindowActorParent { // Called by ZenEdgeScrollManager to send a message to a specific child actor sendEventToChild(browsingContext, messageName, eventData) { if (!browsingContext) { - logParentActor(`sendEventToChild: No browsingContext provided for ${messageName}.`); - return; + logParentActor(`sendEventToChild: No browsingContext provided for ${messageName}.`); + return; } logParentActor(`Parent sending ${messageName} to child in context: ${browsingContext.currentWindowGlobal?.documentURI?.spec}`); this.sendAsyncMessage(messageName, eventData); From 25bc7bdd21cdaf4004bda732fb954e778be15b00 Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Wed, 21 May 2025 21:03:03 +0800 Subject: [PATCH 09/34] Add prefs zen.edgescroll.enabled --- src/browser/app/profile/features.inc | 2 + .../components/preferences/zen-settings.js | 5 + src/zen/edgescroll/ZenEdgeScrollManager.mjs | 204 ++++++------------ 3 files changed, 77 insertions(+), 134 deletions(-) diff --git a/src/browser/app/profile/features.inc b/src/browser/app/profile/features.inc index 83993709f1..637b72cfa8 100644 --- a/src/browser/app/profile/features.inc +++ b/src/browser/app/profile/features.inc @@ -78,6 +78,8 @@ pref('zen.glance.hold-duration', 300); // in ms pref('zen.glance.open-essential-external-links', true); pref('zen.glance.activation-method', 'alt'); // ctrl, alt, shift, none, hold +pref('zen.edgescroll.enabled', true); + pref('zen.view.sidebar-height-throttle', 200); // in ms pref('zen.view.sidebar-expanded.max-width', 500); diff --git a/src/browser/components/preferences/zen-settings.js b/src/browser/components/preferences/zen-settings.js index 512d2e91d0..71900a5cd3 100644 --- a/src/browser/components/preferences/zen-settings.js +++ b/src/browser/components/preferences/zen-settings.js @@ -1169,6 +1169,11 @@ Preferences.addAll([ type: 'bool', default: true, }, + { + id: 'zen.edgescroll.enabled', + type: 'bool', + default: true, + }, { id: 'zen.theme.color-prefs.use-workspace-colors', type: 'bool', diff --git a/src/zen/edgescroll/ZenEdgeScrollManager.mjs b/src/zen/edgescroll/ZenEdgeScrollManager.mjs index fe9574d6af..f013b46f0d 100644 --- a/src/zen/edgescroll/ZenEdgeScrollManager.mjs +++ b/src/zen/edgescroll/ZenEdgeScrollManager.mjs @@ -8,10 +8,8 @@ dump("ZenEdgeScrollManager: " + message + "\n"); } - class ZenEdgeScrollManager { - constructor(windowGlobal) { - this.window = windowGlobal; - this.gBrowser = this.window.gBrowser; + class ZenEdgeScrollManager extends ZenDOMOperatedFeature { + init() { this.isSynthesizingDrag = false; this.dragInitialModel = { targetBrowserDuringDrag: null, @@ -24,23 +22,14 @@ this._boundHandleSyntheticDragEnd = this.handleSyntheticDragEnd.bind(this); this._boundHandleWheel = this.handleWheel.bind(this); - // logManager("Constructor created"); - } - - init() { - if (this.window.gZenEdgeScrollManagerInitialized) { - // logManager("Already initialized for this window."); - return; - } - this.window.gZenEdgeScrollManagerInitialized = true; - this.gBrowser = this.window.gBrowser; - if (!this.gBrowser) { - logManager("No gBrowser found. Cannot initialize."); + if (window.gZenEdgeScrollManagerInitialized) { + logManager("Already initialized for this window."); return; } + window.gZenEdgeScrollManagerInitialized = true; // Create and append the edge scroll trigger div - this.edgeScrollTriggerDiv = this.window.document.createElement("div"); + this.edgeScrollTriggerDiv = window.document.createElement("div"); this.edgeScrollTriggerDiv.id = "zen-edge-scroll-trigger"; Object.assign(this.edgeScrollTriggerDiv.style, { position: "fixed", @@ -52,7 +41,7 @@ userSelect: "none", // backgroundColor: "rgba(255,0,0,0.1)", // For debugging visibility }); - this.window.document.documentElement.appendChild(this.edgeScrollTriggerDiv); + window.document.documentElement.appendChild(this.edgeScrollTriggerDiv); this.edgeScrollTriggerDiv.addEventListener('mousedown', this._boundHandleMouseDown, true); this.edgeScrollTriggerDiv.addEventListener('wheel', this._boundHandleWheel, { capture: true, passive: false }); @@ -69,23 +58,19 @@ } this.edgeScrollTriggerDiv = null; } - // Listeners for drag are on the window, keep them if drag is active, but they are added/removed dynamically this.edgeScrollTriggerDiv.removeEventListener('mousemove', this._boundHandleSyntheticDrag, true); this.edgeScrollTriggerDiv.removeEventListener('mouseup', this._boundHandleSyntheticDragEnd, true); - this.window.gZenEdgeScrollManagerInitialized = false; - logManager("Destroyed, listeners removed, edgeScrollTriggerDiv removed."); + window.gZenEdgeScrollManagerInitialized = false; } _getParentActor() { - if (!this.gBrowser.selectedBrowser.browsingContext.currentWindowGlobal) { - logManager("_getParentActor: No windowGlobalChild on this.window. Returning null."); + if (!gBrowser.selectedBrowser.browsingContext.currentWindowGlobal) { + logManager("_getParentActor: No windowGlobalChild on window. Returning null."); return null; } try { - const actor = this.gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor(ACTOR_NAME); - logManager(`_getParentActor: getActor('${ACTOR_NAME}') returned: ${actor}`); // Original log - if (actor) { - } + const actor = gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor(ACTOR_NAME); + // logManager(`_getParentActor: getActor('${ACTOR_NAME}') returned: ${actor}`); // Original log return actor; } catch (e) { logManager(`_getParentActor: Error in getActor('${ACTOR_NAME}'): ${e}`); @@ -94,35 +79,26 @@ } getGapZoneInfo(event) { - const windowWidth = this.window.innerWidth; - // const eventClientX = event.clientX; // event.clientX is on the trigger div + const windowWidth = window.innerWidth; const eventClientY = event.clientY; - // const isInFarRightWindowEdgeGap = eventClientX > (windowWidth - EDGE_INTERACTION_WIDTH_PX) && event.clientX < windowWidth; - // If event is from edgeScrollTriggerDiv, it's always in the "gap". - // We just need to find the browser adjacent to this event. let potentialTargetBrowser = null; let potentialTargetBrowserRect = null; - if (this.gBrowser && this.gBrowser.browsers) { - for (const browser of this.gBrowser.browsers) { - if (browser.hidden || browser.getAttribute("transparent") === "true" || !browser.getAttribute("primary")) { - continue; - } - - const browserRect = browser.getBoundingClientRect(); - if (browserRect.width === 0 || browserRect.height === 0) continue; + const selectedBrowser = gBrowser.selectedBrowser; + if (selectedBrowser && selectedBrowser.getAttribute("primary") === "true") { + const selectedBrowserRect = selectedBrowser.getBoundingClientRect(); + if (selectedBrowserRect.width > 0 && selectedBrowserRect.height > 0) { // Check if the browser's right edge is very close to the window's right edge - const isBrowserAtRightEdge = (windowWidth - browserRect.right) <= EDGE_INTERACTION_WIDTH_PX + 1; - const isEventYWithinBrowser = eventClientY >= browserRect.top && eventClientY <= browserRect.bottom; - + const isBrowserAtRightEdge = (windowWidth - selectedBrowserRect.right) <= EDGE_INTERACTION_WIDTH_PX + 1; + const isEventYWithinBrowser = eventClientY >= selectedBrowserRect.top && eventClientY <= selectedBrowserRect.bottom; if (isBrowserAtRightEdge && isEventYWithinBrowser) { - potentialTargetBrowser = browser; - potentialTargetBrowserRect = browserRect; - break; // Found the target browser + potentialTargetBrowser = selectedBrowser; + potentialTargetBrowserRect = selectedBrowserRect; } } } + // The event is on the trigger div, so it is "in gap". We return the browser found. return { isInGap: true, targetBrowser: potentialTargetBrowser, browserRect: potentialTargetBrowserRect }; } @@ -130,8 +106,8 @@ createSyntheticEventData(originalEvent, targetBrowserRect, eventType) { const clientXInContent = Math.max(0, Math.floor(targetBrowserRect.width - SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE)); const clientYInContent = Math.max(0, Math.min(Math.floor(originalEvent.clientY - targetBrowserRect.top), Math.floor(targetBrowserRect.height - 1))); - const screenX = Math.floor(this.window.screenX + targetBrowserRect.left + clientXInContent); - const screenY = Math.floor(this.window.screenY + targetBrowserRect.top + clientYInContent); + const screenX = Math.floor(window.screenX + targetBrowserRect.left + clientXInContent); + const screenY = Math.floor(window.screenY + targetBrowserRect.top + clientYInContent); return { type: eventType, clientX: clientXInContent, clientY: clientYInContent, @@ -153,43 +129,33 @@ let targetBrowser = gapInfo.targetBrowser; let targetBrowserRect = gapInfo.browserRect; - // Logic to switch tab if a non-selected browser is targeted - if (targetBrowser !== this.gBrowser.selectedBrowser) { - const tabToSelect = this.gBrowser.getTabForBrowser(targetBrowser); - if (tabToSelect && this.gBrowser.selectedTab !== tabToSelect) { - this.gBrowser.selectedTab = tabToSelect; - // After tab switch, gBrowser.selectedBrowser might take a moment to update, - // or might not be the one we expect if the switch fails or is async. - // It's safer to re-get the selectedBrowser and its rect. - targetBrowser = this.gBrowser.selectedBrowser; - if (targetBrowser) { - targetBrowserRect = targetBrowser.getBoundingClientRect(); - } else { - logManager("Mousedown: Target browser null after tab selection attempt."); return; - } - } - } - // Ensure targetBrowser and its rect are valid after potential tab switch - if (!targetBrowser || !targetBrowserRect || targetBrowserRect.width === 0 || targetBrowserRect.height === 0) { - logManager("Mousedown: Invalid targetBrowser or rect after potential tab switch. Bailing out."); - return; - } - + // // Logic to switch tab if a non-selected browser is targeted + // if (targetBrowser !== gBrowser.selectedBrowser) { + // const tabToSelect = gBrowser.getTabForBrowser(targetBrowser); + // if (tabToSelect && gBrowser.selectedTab !== tabToSelect) { + // gBrowser.selectedTab = tabToSelect; + // // After tab switch, gBrowser.selectedBrowser might take a moment to update, + // // or might not be the one we expect if the switch fails or is async. + // // It's safer to re-get the selectedBrowser and its rect. + // targetBrowser = gBrowser.selectedBrowser; + // if (targetBrowser) { + // targetBrowserRect = targetBrowser.getBoundingClientRect(); + // } else { + // logManager("Mousedown: Target browser null after tab selection attempt."); return; + // } + // } + // } + // // Ensure targetBrowser and its rect are valid after potential tab switch + // if (!targetBrowser || !targetBrowserRect || targetBrowserRect.width === 0 || targetBrowserRect.height === 0) { + // logManager("Mousedown: Invalid targetBrowser or rect after potential tab switch. Bailing out."); + // return; + // } const parentActor = this._getParentActor(); - - // Add a check before calling the method - if (!parentActor) { - logManager("Mousedown: parentActor is null. Bailing out."); - return; - } - if (typeof parentActor.sendEventToChild !== 'function') { - logManager(`Mousedown: parentActor.sendEventToChild is NOT a function. Actual type: ${typeof parentActor.sendEventToChild}. Actor constructor: ${parentActor?.constructor?.name}. Bailing out.`); + if (!parentActor || !targetBrowser.browsingContext) { + logManager("Mousedown: No parentActor or browsingContext for target browser: " + targetBrowser.currentURI?.spec); return; } - if (!targetBrowser?.browsingContext) { - logManager("Mousedown: No targetBrowser.browsingContext. Bailing out. Target: " + targetBrowser?.currentURI?.spec); return; - } event.preventDefault(); this.isSynthesizingDrag = true; @@ -210,7 +176,7 @@ const targetBrowser = this.dragInitialModel.targetBrowserDuringDrag; const targetBrowsingContext = this.dragInitialModel.targetBrowsingContextDuringDrag; - if (this.gBrowser.selectedBrowser !== targetBrowser) { + if (gBrowser.selectedBrowser !== targetBrowser) { // logManager("Drag: Target browser changed. Ending drag."); this.handleSyntheticDragEnd(event); return; } @@ -269,26 +235,8 @@ const targetBrowser = gapInfo.targetBrowser; const targetBrowserRect = gapInfo.browserRect; - - // Optional: If we only want to scroll the active browser when it's the one at the edge. - // However, with the trigger div, it's more intuitive to scroll the browser that is visually there. - // if (targetBrowser !== this.gBrowser.selectedBrowser) { - // logManager("Wheel: Adjacent browser is not the active one. Ignoring wheel for now."); - // return; - // } - const parentActor = this._getParentActor(); - if (!parentActor || !targetBrowser.browsingContext) { - logManager("Wheel: No parentActor or browsingContext for target browser: " + targetBrowser.currentURI?.spec); - return; - } - if (!targetBrowserRect || targetBrowserRect.width === 0 || targetBrowserRect.height === 0) { - logManager("Wheel: Invalid targetBrowserRect for wheel event. Bailing out."); - return; - } - - event.preventDefault(); event.stopPropagation(); const wheelData = { deltaX: event.deltaX, deltaY: event.deltaY, deltaZ: event.deltaZ, deltaMode: event.deltaMode, @@ -311,34 +259,28 @@ // Actor Registration (must happen before manager instantiation if manager relies on actors being ready) // This is modeled after ZenGlanceManager's registerWindowActors function registerEdgeScrollActors() { - const actorConfig = { - parent: { - esModuleURI: 'chrome://browser/content/zen-components/actors/ZenEdgeScrollParent.sys.mjs', - }, - child: { - esModuleURI: 'chrome://browser/content/zen-components/actors/ZenEdgeScrollChild.sys.mjs', - }, - allFrames: true, // Child actor should be available in all frames - matches: [ - '*://*/*', // For general content pages - 'chrome://*/*' // Explicitly allow all chrome URIs - ], - includeChrome: true, // <--- ENSURE THIS LINE IS PRESENT AND SET TO TRUE - }; - - if (window.gZenActorsManager && typeof window.gZenActorsManager.addJSWindowActor === 'function') { - window.gZenActorsManager.addJSWindowActor(ACTOR_NAME, actorConfig); - logManager(`${ACTOR_NAME} actors registered via gZenActorsManager.`); - } else { - try { - ChromeUtils.registerWindowActor(ACTOR_NAME, { // Name must match ACTOR_NAME - parent: { moduleURI: actorConfig.parent.esModuleURI }, - child: { moduleURI: actorConfig.child.esModuleURI }, - matches: actorConfig.matches, - allFrames: actorConfig.allFrames, - }); - logManager(`${ACTOR_NAME} actors registered via ChromeUtils.registerWindowActor.`); - } catch (e) { + if (Services.prefs.getBoolPref('zen.edgescroll.enabled', true)) { + window.gZenEdgeScrollManagerInstance = new ZenEdgeScrollManager(); + + const actorConfig = { + parent: { + esModuleURI: 'chrome://browser/content/zen-components/actors/ZenEdgeScrollParent.sys.mjs', + }, + child: { + esModuleURI: 'chrome://browser/content/zen-components/actors/ZenEdgeScrollChild.sys.mjs', + }, + allFrames: true, // Child actor should be available in all frames + matches: [ + '*://*/*', // For general content pages + 'chrome://*/*' // Explicitly allow all chrome URIs + ], + includeChrome: true, // <--- ENSURE THIS LINE IS PRESENT AND SET TO TRUE + }; + + if (window.gZenActorsManager && typeof window.gZenActorsManager.addJSWindowActor === 'function') { + window.gZenActorsManager.addJSWindowActor(ACTOR_NAME, actorConfig); + logManager(`${ACTOR_NAME} actors registered via gZenActorsManager.`); + } else { console.error(`Failed to register ${ACTOR_NAME} actors:`, e); logManager(`Failed to register ${ACTOR_NAME} actors: ${e}`); } @@ -347,10 +289,4 @@ registerEdgeScrollActors(); - window.gZenEdgeScrollManagerInstance = new ZenEdgeScrollManager(window); - if (document.readyState === "complete" || document.readyState === "interactive") { - window.gZenEdgeScrollManagerInstance.init(); - } else { - window.addEventListener("load", () => window.gZenEdgeScrollManagerInstance.init(), { once: true }); - } } From ae91fc4d6971cfb3e8557a4270df02a8b8a685bd Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Wed, 21 May 2025 21:51:20 +0800 Subject: [PATCH 10/34] fix: Change the mousemove and mouseup listener to window --- src/zen/edgescroll/ZenEdgeScrollManager.mjs | 38 +++------------------ 1 file changed, 4 insertions(+), 34 deletions(-) diff --git a/src/zen/edgescroll/ZenEdgeScrollManager.mjs b/src/zen/edgescroll/ZenEdgeScrollManager.mjs index f013b46f0d..9ed2a97394 100644 --- a/src/zen/edgescroll/ZenEdgeScrollManager.mjs +++ b/src/zen/edgescroll/ZenEdgeScrollManager.mjs @@ -129,28 +129,6 @@ let targetBrowser = gapInfo.targetBrowser; let targetBrowserRect = gapInfo.browserRect; - // // Logic to switch tab if a non-selected browser is targeted - // if (targetBrowser !== gBrowser.selectedBrowser) { - // const tabToSelect = gBrowser.getTabForBrowser(targetBrowser); - // if (tabToSelect && gBrowser.selectedTab !== tabToSelect) { - // gBrowser.selectedTab = tabToSelect; - // // After tab switch, gBrowser.selectedBrowser might take a moment to update, - // // or might not be the one we expect if the switch fails or is async. - // // It's safer to re-get the selectedBrowser and its rect. - // targetBrowser = gBrowser.selectedBrowser; - // if (targetBrowser) { - // targetBrowserRect = targetBrowser.getBoundingClientRect(); - // } else { - // logManager("Mousedown: Target browser null after tab selection attempt."); return; - // } - // } - // } - // // Ensure targetBrowser and its rect are valid after potential tab switch - // if (!targetBrowser || !targetBrowserRect || targetBrowserRect.width === 0 || targetBrowserRect.height === 0) { - // logManager("Mousedown: Invalid targetBrowser or rect after potential tab switch. Bailing out."); - // return; - // } - const parentActor = this._getParentActor(); if (!parentActor || !targetBrowser.browsingContext) { logManager("Mousedown: No parentActor or browsingContext for target browser: " + targetBrowser.currentURI?.spec); @@ -166,8 +144,8 @@ // logManager(`Mousedown: Sending SynthesizeMouseEvent to ${targetBrowser.currentURI?.spec}`); parentActor.sendEventToChild(targetBrowser.browsingContext, "ZenEdgeScroll:SynthesizeMouseEvent", eventData); - this.edgeScrollTriggerDiv.addEventListener('mousemove', this._boundHandleSyntheticDrag, true); - this.edgeScrollTriggerDiv.addEventListener('mouseup', this._boundHandleSyntheticDragEnd, true); + window.addEventListener('mousemove', this._boundHandleSyntheticDrag, true); + window.addEventListener('mouseup', this._boundHandleSyntheticDragEnd, true); } handleSyntheticDrag(event) { @@ -221,8 +199,8 @@ this.isSynthesizingDrag = false; this.dragInitialModel.targetBrowserDuringDrag = null; this.dragInitialModel.targetBrowsingContextDuringDrag = null; - this.edgeScrollTriggerDiv.removeEventListener('mousemove', this._boundHandleSyntheticDrag, true); - this.edgeScrollTriggerDiv.removeEventListener('mouseup', this._boundHandleSyntheticDragEnd, true); + window.removeEventListener('mousemove', this._boundHandleSyntheticDrag, true); + window.removeEventListener('mouseup', this._boundHandleSyntheticDragEnd, true); } handleWheel(event) { @@ -249,13 +227,6 @@ } } - // Initialization and Actor Registration - // This should be called once per window, typically during browser startup. - // Adapt this to how Zen Desktop initializes its managers and registers actors. - // if (window.gZenEdgeScrollManagerInstance) { - // return; - // } - // Actor Registration (must happen before manager instantiation if manager relies on actors being ready) // This is modeled after ZenGlanceManager's registerWindowActors function registerEdgeScrollActors() { @@ -272,7 +243,6 @@ allFrames: true, // Child actor should be available in all frames matches: [ '*://*/*', // For general content pages - 'chrome://*/*' // Explicitly allow all chrome URIs ], includeChrome: true, // <--- ENSURE THIS LINE IS PRESENT AND SET TO TRUE }; From 84aa0043297a3f38af4b5b3a8e10a78ddb427c86 Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Wed, 21 May 2025 22:02:49 +0800 Subject: [PATCH 11/34] fix: Add restart prompt when updating edgescroll prefs --- src/browser/components/preferences/zen-settings.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/browser/components/preferences/zen-settings.js b/src/browser/components/preferences/zen-settings.js index 71900a5cd3..0c1817ccfd 100644 --- a/src/browser/components/preferences/zen-settings.js +++ b/src/browser/components/preferences/zen-settings.js @@ -777,6 +777,7 @@ var gZenWorkspacesSettings = { }; Services.prefs.addObserver('zen.tab-unloader.enabled', tabsUnloaderPrefListener); Services.prefs.addObserver('zen.glance.enabled', tabsUnloaderPrefListener); // We can use the same listener for both prefs + Services.prefs.addObserver('zen.edgescroll.enabled', tabsUnloaderPrefListener); Services.prefs.addObserver( 'zen.workspaces.container-specific-essentials-enabled', tabsUnloaderPrefListener @@ -785,6 +786,7 @@ var gZenWorkspacesSettings = { window.addEventListener('unload', () => { Services.prefs.removeObserver('zen.tab-unloader.enabled', tabsUnloaderPrefListener); Services.prefs.removeObserver('zen.glance.enabled', tabsUnloaderPrefListener); + Services.prefs.removeObserver('zen.edgescroll.enabled', tabsUnloaderPrefListener); Services.prefs.removeObserver('zen.glance.activation-method', tabsUnloaderPrefListener); Services.prefs.removeObserver( 'zen.workspaces.container-specific-essentials-enabled', From ee0a312b88a9ac684dfcabfdd5a512c6b853098b Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Wed, 21 May 2025 22:19:27 +0800 Subject: [PATCH 12/34] fix: Enable edgescroll on about:* pages --- src/zen/edgescroll/ZenEdgeScrollManager.mjs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/zen/edgescroll/ZenEdgeScrollManager.mjs b/src/zen/edgescroll/ZenEdgeScrollManager.mjs index 9ed2a97394..ae3baa6e8c 100644 --- a/src/zen/edgescroll/ZenEdgeScrollManager.mjs +++ b/src/zen/edgescroll/ZenEdgeScrollManager.mjs @@ -1,4 +1,3 @@ -/* global window, document, gBrowser, Services, ChromeUtils */ // Assuming gBrowser etc. are available { const EDGE_INTERACTION_WIDTH_PX = Services.prefs.getIntPref("zen.theme.border-radius", 8); const SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE = 1; @@ -240,9 +239,10 @@ child: { esModuleURI: 'chrome://browser/content/zen-components/actors/ZenEdgeScrollChild.sys.mjs', }, - allFrames: true, // Child actor should be available in all frames + allFrames: true, matches: [ - '*://*/*', // For general content pages + '*://*/*', + 'about:*', // For about: pages ], includeChrome: true, // <--- ENSURE THIS LINE IS PRESENT AND SET TO TRUE }; From 892c6a8d5a5d4986947b98c4ce01add045bc381c Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Thu, 22 May 2025 02:03:21 +0800 Subject: [PATCH 13/34] fix: Only display zen-edge-scroll-trigger when the sidebar is on the left --- src/zen/edgescroll/ZenEdgeScrollManager.mjs | 23 ++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/zen/edgescroll/ZenEdgeScrollManager.mjs b/src/zen/edgescroll/ZenEdgeScrollManager.mjs index ae3baa6e8c..a802ec0376 100644 --- a/src/zen/edgescroll/ZenEdgeScrollManager.mjs +++ b/src/zen/edgescroll/ZenEdgeScrollManager.mjs @@ -20,6 +20,7 @@ this._boundHandleSyntheticDrag = this.handleSyntheticDrag.bind(this); this._boundHandleSyntheticDragEnd = this.handleSyntheticDragEnd.bind(this); this._boundHandleWheel = this.handleWheel.bind(this); + this._boundUpdateTriggerDivDisplay = this._updateTriggerDivDisplay.bind(this); // Added if (window.gZenEdgeScrollManagerInitialized) { logManager("Already initialized for this window."); @@ -45,6 +46,9 @@ this.edgeScrollTriggerDiv.addEventListener('mousedown', this._boundHandleMouseDown, true); this.edgeScrollTriggerDiv.addEventListener('wheel', this._boundHandleWheel, { capture: true, passive: false }); + this._updateTriggerDivDisplay(); // Added: Set initial display state + Services.prefs.addObserver("zen.tabs.vertical.right-side", this._boundUpdateTriggerDivDisplay); // Added: Observe preference + logManager("Initialized, edgeScrollTriggerDiv created, and event listeners added."); } @@ -53,15 +57,28 @@ this.edgeScrollTriggerDiv.removeEventListener('mousedown', this._boundHandleMouseDown, true); this.edgeScrollTriggerDiv.removeEventListener('wheel', this._boundHandleWheel, true); if (this.edgeScrollTriggerDiv.parentNode) { - this.edgeScrollTriggerDiv.parentNode.removeChild(this.edgeScrollTriggerDiv); + this.edgeScrollTriggerDiv.parentNode.removeChild(this.edgeScrollTriggerDiv); // Corrected removeChild call } this.edgeScrollTriggerDiv = null; } - this.edgeScrollTriggerDiv.removeEventListener('mousemove', this._boundHandleSyntheticDrag, true); - this.edgeScrollTriggerDiv.removeEventListener('mouseup', this._boundHandleSyntheticDragEnd, true); + // These listeners are added to window, not edgeScrollTriggerDiv in handleMouseDown + window.removeEventListener('mousemove', this._boundHandleSyntheticDrag, true); + window.removeEventListener('mouseup', this._boundHandleSyntheticDragEnd, true); + Services.prefs.removeObserver("zen.tabs.vertical.right-side", this._boundUpdateTriggerDivDisplay); // Added: Remove observer window.gZenEdgeScrollManagerInitialized = false; } + _updateTriggerDivDisplay() { // Added method + if (!this.edgeScrollTriggerDiv) { + return; + } + if (window.gZenCompactModeManager && gZenCompactModeManager.sidebarIsOnRight) { + this.edgeScrollTriggerDiv.style.display = "none"; + } else { + this.edgeScrollTriggerDiv.style.display = "block"; + } + } + _getParentActor() { if (!gBrowser.selectedBrowser.browsingContext.currentWindowGlobal) { logManager("_getParentActor: No windowGlobalChild on window. Returning null."); From 8868a4cfe50245170cd98e5b3a7b4f9dfe4bddfd Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Thu, 22 May 2025 02:35:36 +0800 Subject: [PATCH 14/34] fix: Increase offset to 2 pixels --- src/zen/edgescroll/ZenEdgeScrollManager.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zen/edgescroll/ZenEdgeScrollManager.mjs b/src/zen/edgescroll/ZenEdgeScrollManager.mjs index a802ec0376..a71a98015c 100644 --- a/src/zen/edgescroll/ZenEdgeScrollManager.mjs +++ b/src/zen/edgescroll/ZenEdgeScrollManager.mjs @@ -1,6 +1,6 @@ { const EDGE_INTERACTION_WIDTH_PX = Services.prefs.getIntPref("zen.theme.border-radius", 8); - const SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE = 1; + const SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE = 2; const ACTOR_NAME = "ZenEdgeScroll"; // Name used for actor registration function logManager(message) { From ebea8f6625fe3a18f599992ce5a531ec58028e70 Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Thu, 22 May 2025 03:31:37 +0800 Subject: [PATCH 15/34] chore: Remove logging --- src/zen/edgescroll/ZenEdgeScrollManager.mjs | 24 +------------------ .../actors/ZenEdgeScrollChild.sys.mjs | 17 ++----------- .../actors/ZenEdgeScrollParent.sys.mjs | 9 ------- 3 files changed, 3 insertions(+), 47 deletions(-) diff --git a/src/zen/edgescroll/ZenEdgeScrollManager.mjs b/src/zen/edgescroll/ZenEdgeScrollManager.mjs index a71a98015c..c36eec5d3f 100644 --- a/src/zen/edgescroll/ZenEdgeScrollManager.mjs +++ b/src/zen/edgescroll/ZenEdgeScrollManager.mjs @@ -3,10 +3,6 @@ const SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE = 2; const ACTOR_NAME = "ZenEdgeScroll"; // Name used for actor registration - function logManager(message) { - dump("ZenEdgeScrollManager: " + message + "\n"); - } - class ZenEdgeScrollManager extends ZenDOMOperatedFeature { init() { this.isSynthesizingDrag = false; @@ -23,7 +19,6 @@ this._boundUpdateTriggerDivDisplay = this._updateTriggerDivDisplay.bind(this); // Added if (window.gZenEdgeScrollManagerInitialized) { - logManager("Already initialized for this window."); return; } window.gZenEdgeScrollManagerInitialized = true; @@ -48,8 +43,6 @@ this._updateTriggerDivDisplay(); // Added: Set initial display state Services.prefs.addObserver("zen.tabs.vertical.right-side", this._boundUpdateTriggerDivDisplay); // Added: Observe preference - - logManager("Initialized, edgeScrollTriggerDiv created, and event listeners added."); } destroy() { @@ -81,15 +74,13 @@ _getParentActor() { if (!gBrowser.selectedBrowser.browsingContext.currentWindowGlobal) { - logManager("_getParentActor: No windowGlobalChild on window. Returning null."); return null; } try { const actor = gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor(ACTOR_NAME); - // logManager(`_getParentActor: getActor('${ACTOR_NAME}') returned: ${actor}`); // Original log return actor; } catch (e) { - logManager(`_getParentActor: Error in getActor('${ACTOR_NAME}'): ${e}`); + console.error(`Error getting actor ${ACTOR_NAME}:`, e); return null; } } @@ -139,7 +130,6 @@ const gapInfo = this.getGapZoneInfo(event); // event is from edgeScrollTriggerDiv if (!gapInfo.targetBrowser) { - // logManager("Mousedown: Event on trigger div, but no specific adjacent browser."); return; } let targetBrowser = gapInfo.targetBrowser; @@ -147,7 +137,6 @@ const parentActor = this._getParentActor(); if (!parentActor || !targetBrowser.browsingContext) { - logManager("Mousedown: No parentActor or browsingContext for target browser: " + targetBrowser.currentURI?.spec); return; } @@ -157,7 +146,6 @@ this.dragInitialModel.targetBrowsingContextDuringDrag = targetBrowser.browsingContext; const eventData = this.createSyntheticEventData(event, targetBrowserRect, 'mousedown'); - // logManager(`Mousedown: Sending SynthesizeMouseEvent to ${targetBrowser.currentURI?.spec}`); parentActor.sendEventToChild(targetBrowser.browsingContext, "ZenEdgeScroll:SynthesizeMouseEvent", eventData); window.addEventListener('mousemove', this._boundHandleSyntheticDrag, true); @@ -171,20 +159,17 @@ const targetBrowsingContext = this.dragInitialModel.targetBrowsingContextDuringDrag; if (gBrowser.selectedBrowser !== targetBrowser) { - // logManager("Drag: Target browser changed. Ending drag."); this.handleSyntheticDragEnd(event); return; } const parentActor = this._getParentActor(); if (!parentActor) { - logManager("Drag: No parentActor. Ending drag."); this.handleSyntheticDragEnd(event); return; } event.preventDefault(); event.stopPropagation(); const currentTargetBrowserRect = targetBrowser.getBoundingClientRect(); if (currentTargetBrowserRect.width === 0 || currentTargetBrowserRect.height === 0) { - // logManager("Drag: Target browser rect is zero. Ending drag."); this.handleSyntheticDragEnd(event); return; } const eventData = this.createSyntheticEventData(event, currentTargetBrowserRect, 'mousemove'); @@ -202,13 +187,10 @@ const currentTargetBrowserRect = targetBrowser.getBoundingClientRect(); if (currentTargetBrowserRect.width > 0 && currentTargetBrowserRect.height > 0) { const eventData = this.createSyntheticEventData(event, currentTargetBrowserRect, 'mouseup'); - // logManager(`DragEnd: Sending SynthesizeMouseEvent to ${targetBrowser?.currentURI?.spec}`); parentActor.sendEventToChild(targetBrowsingContext, "ZenEdgeScroll:SynthesizeMouseEvent", eventData); } else { - // logManager("DragEnd: Target browser rect is zero, cannot send mouseup."); } } else if (parentActor && !event) { // Called without event (e.g. drag cancelled) - // logManager("DragEnd: Called without event, mouseup not synthesized via event data."); // Optionally send a generic mouseup if needed, or just clean up. } } @@ -223,7 +205,6 @@ const gapInfo = this.getGapZoneInfo(event); // event is from edgeScrollTriggerDiv if (!gapInfo.targetBrowser) { - // logManager("Wheel: Event on trigger div, but no specific adjacent browser."); return; } @@ -238,7 +219,6 @@ clientX: Math.max(0, Math.floor(targetBrowserRect.width - SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE)), clientY: Math.max(0, Math.min(Math.floor(event.clientY - targetBrowserRect.top), Math.floor(targetBrowserRect.height - 1))) }; - // logManager(`Wheel: Sending DispatchWheel to ${targetBrowser.currentURI?.spec}`); parentActor.sendEventToChild(targetBrowser.browsingContext, "ZenEdgeScroll:DispatchWheel", { wheelData }); } } @@ -266,10 +246,8 @@ if (window.gZenActorsManager && typeof window.gZenActorsManager.addJSWindowActor === 'function') { window.gZenActorsManager.addJSWindowActor(ACTOR_NAME, actorConfig); - logManager(`${ACTOR_NAME} actors registered via gZenActorsManager.`); } else { console.error(`Failed to register ${ACTOR_NAME} actors:`, e); - logManager(`Failed to register ${ACTOR_NAME} actors: ${e}`); } } } diff --git a/src/zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs b/src/zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs index 40b015dbb3..0b45590da5 100644 --- a/src/zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs +++ b/src/zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs @@ -3,17 +3,12 @@ const { utils: Cu, interfaces: Ci } = Components; -function logChild(message) { - // dump("ZenEdgeScrollChild: " + message + "\n"); -} - export class ZenEdgeScrollChild extends JSWindowActorChild { constructor() { super(); } receiveMessage(message) { - logChild(`Received message in child: ${message.name} for URL: ${this.contentWindow?.document?.location?.href || "unknown"}`); switch (message.name) { case "ZenEdgeScroll:SynthesizeMouseEvent": this.handleSynthesizeMouseEvent(message.data); @@ -22,19 +17,16 @@ export class ZenEdgeScrollChild extends JSWindowActorChild { this.handleDispatchWheel(message.data); break; default: - logChild(`Unknown message received: ${message.name}`); } } handleSynthesizeMouseEvent(data) { if (!data || !data.type) { - logChild("SynthesizeMouseEvent: Invalid data received."); return; } const contentWin = this.contentWindow; if (!contentWin || !contentWin.windowUtils) { - logChild("SynthesizeMouseEvent: content.windowUtils is not available. URL: " + (contentWin?.document?.location?.href || "unknown")); return; } @@ -54,25 +46,22 @@ export class ZenEdgeScrollChild extends JSWindowActorChild { } try { - // logChild(`SynthesizeMouseEvent: Dispatching ${data.type} to ${contentWin.document.location.href} at X:${data.clientX}, Y:${data.clientY}`); contentWin.windowUtils.sendMouseEvent( data.type, data.clientX, data.clientY, data.button, clickCount, modifiers, false, 0.5, Ci.nsIDOMWindowUtils.INPUT_SOURCE_MOUSE, false ); } catch (e) { - logChild(`Error dispatching trusted synthetic ${data.type} event: ${e} - ${e.stack}. URL: ` + (contentWin?.document?.location?.href || "unknown")); + console.error("Error dispatching mouse event:", e); } } handleDispatchWheel({ wheelData }) { if (!wheelData) { - logChild("DispatchWheel: No wheelData received."); return; } const contentWin = this.contentWindow; if (!contentWin || !contentWin.windowUtils) { - logChild("DispatchWheel: content.windowUtils is not available. URL: " + (contentWin?.document?.location?.href || "unknown")); return; } const doc = contentWin.document; @@ -88,18 +77,16 @@ export class ZenEdgeScrollChild extends JSWindowActorChild { if (wheelData.shiftKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_SHIFT; try { - // logChild(`DispatchWheel: Dispatching to ${contentWin.document.location.href} at X:${clientX}, Y:${clientY}`); contentWin.windowUtils.sendWheelEvent( clientX, clientY, wheelData.deltaX, wheelData.deltaY, wheelData.deltaZ, wheelData.deltaMode, modifiers, 0, 0, true, false, false, false, false ); } catch (e) { - logChild(`Error dispatching trusted wheel event: ${e} - ${e.stack}. URL: ` + (contentWin?.document?.location?.href || "unknown")); + console.error("Error dispatching wheel event:", e); } } destroy() { - // logChild("Destroying ZenEdgeScrollChild for " + (this.contentWindow?.document?.location?.href || "unknown")); super.destroy(); } } \ No newline at end of file diff --git a/src/zen/edgescroll/actors/ZenEdgeScrollParent.sys.mjs b/src/zen/edgescroll/actors/ZenEdgeScrollParent.sys.mjs index da00b802f0..b7ab588478 100644 --- a/src/zen/edgescroll/actors/ZenEdgeScrollParent.sys.mjs +++ b/src/zen/edgescroll/actors/ZenEdgeScrollParent.sys.mjs @@ -2,35 +2,26 @@ console.log("ZenEdgeScrollParent: " + "alive" + "\n"); -function logParentActor(message) { - // dump("ZenEdgeScrollParentActor: " + message + "\n"); -} - export class ZenEdgeScrollParent extends JSWindowActorParent { constructor() { super(); - // logParentActor("Constructor"); } // This actor primarily sends messages to its children. // It might receive messages if a child needs to query the parent for info. async receiveMessage(message) { - logParentActor(`Parent received message: ${message.name} from child in ${message.browsingContext?.currentWindowGlobal?.documentURI?.spec}`); // Handle any messages from child if needed in the future } // Called by ZenEdgeScrollManager to send a message to a specific child actor sendEventToChild(browsingContext, messageName, eventData) { if (!browsingContext) { - logParentActor(`sendEventToChild: No browsingContext provided for ${messageName}.`); return; } - logParentActor(`Parent sending ${messageName} to child in context: ${browsingContext.currentWindowGlobal?.documentURI?.spec}`); this.sendAsyncMessage(messageName, eventData); } destroy() { - // logParentActor("Destroying ZenEdgeScrollParent"); super.destroy(); } } \ No newline at end of file From c1000687871a03ee0e33636c5b5314eb0c818f39 Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Thu, 22 May 2025 12:01:59 +0800 Subject: [PATCH 16/34] style: Lint code --- .../components/preferences/zen-settings.js | 2 +- src/zen/common/zen-sets.js | 2 +- src/zen/edgescroll/ZenEdgeScrollManager.mjs | 182 +++++++++++++----- .../actors/ZenEdgeScrollChild.sys.mjs | 54 ++++-- .../actors/ZenEdgeScrollParent.sys.mjs | 4 +- 5 files changed, 173 insertions(+), 71 deletions(-) diff --git a/src/browser/components/preferences/zen-settings.js b/src/browser/components/preferences/zen-settings.js index 0c1817ccfd..098fc2faaf 100644 --- a/src/browser/components/preferences/zen-settings.js +++ b/src/browser/components/preferences/zen-settings.js @@ -777,7 +777,7 @@ var gZenWorkspacesSettings = { }; Services.prefs.addObserver('zen.tab-unloader.enabled', tabsUnloaderPrefListener); Services.prefs.addObserver('zen.glance.enabled', tabsUnloaderPrefListener); // We can use the same listener for both prefs - Services.prefs.addObserver('zen.edgescroll.enabled', tabsUnloaderPrefListener); + Services.prefs.addObserver('zen.edgescroll.enabled', tabsUnloaderPrefListener); Services.prefs.addObserver( 'zen.workspaces.container-specific-essentials-enabled', tabsUnloaderPrefListener diff --git a/src/zen/common/zen-sets.js b/src/zen/common/zen-sets.js index 8b7c5e5dfa..1f73018f32 100644 --- a/src/zen/common/zen-sets.js +++ b/src/zen/common/zen-sets.js @@ -107,4 +107,4 @@ document.addEventListener( }); }, { once: true } -); \ No newline at end of file +); diff --git a/src/zen/edgescroll/ZenEdgeScrollManager.mjs b/src/zen/edgescroll/ZenEdgeScrollManager.mjs index c36eec5d3f..728f90efe5 100644 --- a/src/zen/edgescroll/ZenEdgeScrollManager.mjs +++ b/src/zen/edgescroll/ZenEdgeScrollManager.mjs @@ -1,7 +1,7 @@ { - const EDGE_INTERACTION_WIDTH_PX = Services.prefs.getIntPref("zen.theme.border-radius", 8); + const EDGE_INTERACTION_WIDTH_PX = Services.prefs.getIntPref('zen.theme.border-radius', 8); const SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE = 2; - const ACTOR_NAME = "ZenEdgeScroll"; // Name used for actor registration + const ACTOR_NAME = 'ZenEdgeScroll'; // Name used for actor registration class ZenEdgeScrollManager extends ZenDOMOperatedFeature { init() { @@ -24,30 +24,40 @@ window.gZenEdgeScrollManagerInitialized = true; // Create and append the edge scroll trigger div - this.edgeScrollTriggerDiv = window.document.createElement("div"); - this.edgeScrollTriggerDiv.id = "zen-edge-scroll-trigger"; + this.edgeScrollTriggerDiv = window.document.createElement('div'); + this.edgeScrollTriggerDiv.id = 'zen-edge-scroll-trigger'; Object.assign(this.edgeScrollTriggerDiv.style, { - position: "fixed", - top: "0px", - right: "0px", + position: 'fixed', + top: '0px', + right: '0px', width: `${EDGE_INTERACTION_WIDTH_PX}px`, - height: "100%", - zIndex: "2147483647", // Max z-index - userSelect: "none", + height: '100%', + zIndex: '2147483647', // Max z-index + userSelect: 'none', // backgroundColor: "rgba(255,0,0,0.1)", // For debugging visibility }); window.document.documentElement.appendChild(this.edgeScrollTriggerDiv); this.edgeScrollTriggerDiv.addEventListener('mousedown', this._boundHandleMouseDown, true); - this.edgeScrollTriggerDiv.addEventListener('wheel', this._boundHandleWheel, { capture: true, passive: false }); + this.edgeScrollTriggerDiv.addEventListener('wheel', this._boundHandleWheel, { + capture: true, + passive: false, + }); this._updateTriggerDivDisplay(); // Added: Set initial display state - Services.prefs.addObserver("zen.tabs.vertical.right-side", this._boundUpdateTriggerDivDisplay); // Added: Observe preference + Services.prefs.addObserver( + 'zen.tabs.vertical.right-side', + this._boundUpdateTriggerDivDisplay + ); // Added: Observe preference } destroy() { if (this.edgeScrollTriggerDiv) { - this.edgeScrollTriggerDiv.removeEventListener('mousedown', this._boundHandleMouseDown, true); + this.edgeScrollTriggerDiv.removeEventListener( + 'mousedown', + this._boundHandleMouseDown, + true + ); this.edgeScrollTriggerDiv.removeEventListener('wheel', this._boundHandleWheel, true); if (this.edgeScrollTriggerDiv.parentNode) { this.edgeScrollTriggerDiv.parentNode.removeChild(this.edgeScrollTriggerDiv); // Corrected removeChild call @@ -57,18 +67,22 @@ // These listeners are added to window, not edgeScrollTriggerDiv in handleMouseDown window.removeEventListener('mousemove', this._boundHandleSyntheticDrag, true); window.removeEventListener('mouseup', this._boundHandleSyntheticDragEnd, true); - Services.prefs.removeObserver("zen.tabs.vertical.right-side", this._boundUpdateTriggerDivDisplay); // Added: Remove observer + Services.prefs.removeObserver( + 'zen.tabs.vertical.right-side', + this._boundUpdateTriggerDivDisplay + ); // Added: Remove observer window.gZenEdgeScrollManagerInitialized = false; } - _updateTriggerDivDisplay() { // Added method + _updateTriggerDivDisplay() { + // Added method if (!this.edgeScrollTriggerDiv) { return; } if (window.gZenCompactModeManager && gZenCompactModeManager.sidebarIsOnRight) { - this.edgeScrollTriggerDiv.style.display = "none"; + this.edgeScrollTriggerDiv.style.display = 'none'; } else { - this.edgeScrollTriggerDiv.style.display = "block"; + this.edgeScrollTriggerDiv.style.display = 'block'; } } @@ -77,7 +91,8 @@ return null; } try { - const actor = gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor(ACTOR_NAME); + const actor = + gBrowser.selectedBrowser.browsingContext.currentWindowGlobal.getActor(ACTOR_NAME); return actor; } catch (e) { console.error(`Error getting actor ${ACTOR_NAME}:`, e); @@ -93,12 +108,14 @@ let potentialTargetBrowserRect = null; const selectedBrowser = gBrowser.selectedBrowser; - if (selectedBrowser && selectedBrowser.getAttribute("primary") === "true") { + if (selectedBrowser && selectedBrowser.getAttribute('primary') === 'true') { const selectedBrowserRect = selectedBrowser.getBoundingClientRect(); if (selectedBrowserRect.width > 0 && selectedBrowserRect.height > 0) { // Check if the browser's right edge is very close to the window's right edge - const isBrowserAtRightEdge = (windowWidth - selectedBrowserRect.right) <= EDGE_INTERACTION_WIDTH_PX + 1; - const isEventYWithinBrowser = eventClientY >= selectedBrowserRect.top && eventClientY <= selectedBrowserRect.bottom; + const isBrowserAtRightEdge = + windowWidth - selectedBrowserRect.right <= EDGE_INTERACTION_WIDTH_PX + 1; + const isEventYWithinBrowser = + eventClientY >= selectedBrowserRect.top && eventClientY <= selectedBrowserRect.bottom; if (isBrowserAtRightEdge && isEventYWithinBrowser) { potentialTargetBrowser = selectedBrowser; potentialTargetBrowserRect = selectedBrowserRect; @@ -107,21 +124,40 @@ } // The event is on the trigger div, so it is "in gap". We return the browser found. - return { isInGap: true, targetBrowser: potentialTargetBrowser, browserRect: potentialTargetBrowserRect }; + return { + isInGap: true, + targetBrowser: potentialTargetBrowser, + browserRect: potentialTargetBrowserRect, + }; } createSyntheticEventData(originalEvent, targetBrowserRect, eventType) { - const clientXInContent = Math.max(0, Math.floor(targetBrowserRect.width - SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE)); - const clientYInContent = Math.max(0, Math.min(Math.floor(originalEvent.clientY - targetBrowserRect.top), Math.floor(targetBrowserRect.height - 1))); + const clientXInContent = Math.max( + 0, + Math.floor(targetBrowserRect.width - SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE) + ); + const clientYInContent = Math.max( + 0, + Math.min( + Math.floor(originalEvent.clientY - targetBrowserRect.top), + Math.floor(targetBrowserRect.height - 1) + ) + ); const screenX = Math.floor(window.screenX + targetBrowserRect.left + clientXInContent); const screenY = Math.floor(window.screenY + targetBrowserRect.top + clientYInContent); return { - type: eventType, clientX: clientXInContent, clientY: clientYInContent, - screenX: screenX, screenY: screenY, button: originalEvent.button, - buttons: (eventType === 'mousemove' || eventType === 'mousedown') ? 1 : 0, - ctrlKey: originalEvent.ctrlKey, altKey: originalEvent.altKey, - shiftKey: originalEvent.shiftKey, metaKey: originalEvent.metaKey, + type: eventType, + clientX: clientXInContent, + clientY: clientYInContent, + screenX: screenX, + screenY: screenY, + button: originalEvent.button, + buttons: eventType === 'mousemove' || eventType === 'mousedown' ? 1 : 0, + ctrlKey: originalEvent.ctrlKey, + altKey: originalEvent.altKey, + shiftKey: originalEvent.shiftKey, + metaKey: originalEvent.metaKey, }; } @@ -146,34 +182,47 @@ this.dragInitialModel.targetBrowsingContextDuringDrag = targetBrowser.browsingContext; const eventData = this.createSyntheticEventData(event, targetBrowserRect, 'mousedown'); - parentActor.sendEventToChild(targetBrowser.browsingContext, "ZenEdgeScroll:SynthesizeMouseEvent", eventData); + parentActor.sendEventToChild( + targetBrowser.browsingContext, + 'ZenEdgeScroll:SynthesizeMouseEvent', + eventData + ); window.addEventListener('mousemove', this._boundHandleSyntheticDrag, true); window.addEventListener('mouseup', this._boundHandleSyntheticDragEnd, true); } handleSyntheticDrag(event) { - if (!this.isSynthesizingDrag || !this.dragInitialModel.targetBrowsingContextDuringDrag) return; + if (!this.isSynthesizingDrag || !this.dragInitialModel.targetBrowsingContextDuringDrag) + return; const targetBrowser = this.dragInitialModel.targetBrowserDuringDrag; const targetBrowsingContext = this.dragInitialModel.targetBrowsingContextDuringDrag; if (gBrowser.selectedBrowser !== targetBrowser) { - this.handleSyntheticDragEnd(event); return; + this.handleSyntheticDragEnd(event); + return; } const parentActor = this._getParentActor(); if (!parentActor) { - this.handleSyntheticDragEnd(event); return; + this.handleSyntheticDragEnd(event); + return; } - event.preventDefault(); event.stopPropagation(); + event.preventDefault(); + event.stopPropagation(); const currentTargetBrowserRect = targetBrowser.getBoundingClientRect(); if (currentTargetBrowserRect.width === 0 || currentTargetBrowserRect.height === 0) { - this.handleSyntheticDragEnd(event); return; + this.handleSyntheticDragEnd(event); + return; } const eventData = this.createSyntheticEventData(event, currentTargetBrowserRect, 'mousemove'); - parentActor.sendEventToChild(targetBrowsingContext, "ZenEdgeScroll:SynthesizeMouseEvent", eventData); + parentActor.sendEventToChild( + targetBrowsingContext, + 'ZenEdgeScroll:SynthesizeMouseEvent', + eventData + ); } handleSyntheticDragEnd(event) { @@ -182,15 +231,26 @@ const targetBrowsingContext = this.dragInitialModel.targetBrowsingContextDuringDrag; const parentActor = this._getParentActor(); - if (parentActor && event) { // If called by an event - event.preventDefault(); event.stopPropagation(); + if (parentActor && event) { + // If called by an event + event.preventDefault(); + event.stopPropagation(); const currentTargetBrowserRect = targetBrowser.getBoundingClientRect(); if (currentTargetBrowserRect.width > 0 && currentTargetBrowserRect.height > 0) { - const eventData = this.createSyntheticEventData(event, currentTargetBrowserRect, 'mouseup'); - parentActor.sendEventToChild(targetBrowsingContext, "ZenEdgeScroll:SynthesizeMouseEvent", eventData); + const eventData = this.createSyntheticEventData( + event, + currentTargetBrowserRect, + 'mouseup' + ); + parentActor.sendEventToChild( + targetBrowsingContext, + 'ZenEdgeScroll:SynthesizeMouseEvent', + eventData + ); } else { } - } else if (parentActor && !event) { // Called without event (e.g. drag cancelled) + } else if (parentActor && !event) { + // Called without event (e.g. drag cancelled) // Optionally send a generic mouseup if needed, or just clean up. } } @@ -212,14 +272,32 @@ const targetBrowserRect = gapInfo.browserRect; const parentActor = this._getParentActor(); - event.preventDefault(); event.stopPropagation(); + event.preventDefault(); + event.stopPropagation(); const wheelData = { - deltaX: event.deltaX, deltaY: event.deltaY, deltaZ: event.deltaZ, deltaMode: event.deltaMode, - ctrlKey: event.ctrlKey, altKey: event.altKey, shiftKey: event.shiftKey, metaKey: event.metaKey, - clientX: Math.max(0, Math.floor(targetBrowserRect.width - SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE)), - clientY: Math.max(0, Math.min(Math.floor(event.clientY - targetBrowserRect.top), Math.floor(targetBrowserRect.height - 1))) + deltaX: event.deltaX, + deltaY: event.deltaY, + deltaZ: event.deltaZ, + deltaMode: event.deltaMode, + ctrlKey: event.ctrlKey, + altKey: event.altKey, + shiftKey: event.shiftKey, + metaKey: event.metaKey, + clientX: Math.max( + 0, + Math.floor(targetBrowserRect.width - SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE) + ), + clientY: Math.max( + 0, + Math.min( + Math.floor(event.clientY - targetBrowserRect.top), + Math.floor(targetBrowserRect.height - 1) + ) + ), }; - parentActor.sendEventToChild(targetBrowser.browsingContext, "ZenEdgeScroll:DispatchWheel", { wheelData }); + parentActor.sendEventToChild(targetBrowser.browsingContext, 'ZenEdgeScroll:DispatchWheel', { + wheelData, + }); } } @@ -239,12 +317,15 @@ allFrames: true, matches: [ '*://*/*', - 'about:*', // For about: pages + 'about:*', // For about: pages ], - includeChrome: true, // <--- ENSURE THIS LINE IS PRESENT AND SET TO TRUE + includeChrome: true, // <--- ENSURE THIS LINE IS PRESENT AND SET TO TRUE }; - if (window.gZenActorsManager && typeof window.gZenActorsManager.addJSWindowActor === 'function') { + if ( + window.gZenActorsManager && + typeof window.gZenActorsManager.addJSWindowActor === 'function' + ) { window.gZenActorsManager.addJSWindowActor(ACTOR_NAME, actorConfig); } else { console.error(`Failed to register ${ACTOR_NAME} actors:`, e); @@ -253,5 +334,4 @@ } registerEdgeScrollActors(); - } diff --git a/src/zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs b/src/zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs index 0b45590da5..f013d5245a 100644 --- a/src/zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs +++ b/src/zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs @@ -10,10 +10,10 @@ export class ZenEdgeScrollChild extends JSWindowActorChild { receiveMessage(message) { switch (message.name) { - case "ZenEdgeScroll:SynthesizeMouseEvent": + case 'ZenEdgeScroll:SynthesizeMouseEvent': this.handleSynthesizeMouseEvent(message.data); break; - case "ZenEdgeScroll:DispatchWheel": + case 'ZenEdgeScroll:DispatchWheel': this.handleDispatchWheel(message.data); break; default: @@ -36,23 +36,30 @@ export class ZenEdgeScrollChild extends JSWindowActorChild { if (data.metaKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_META; if (data.shiftKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_SHIFT; - if (data.type === "mousemove" && data.buttons === 1) { + if (data.type === 'mousemove' && data.buttons === 1) { modifiers |= Ci.nsIDOMWindowUtils.BUTTON_PRIMARY_ACTION; } let clickCount = 0; - if (data.type === "mousedown" || data.type === "mouseup") { + if (data.type === 'mousedown' || data.type === 'mouseup') { clickCount = 1; } try { contentWin.windowUtils.sendMouseEvent( - data.type, data.clientX, data.clientY, data.button, - clickCount, modifiers, false, 0.5, - Ci.nsIDOMWindowUtils.INPUT_SOURCE_MOUSE, false + data.type, + data.clientX, + data.clientY, + data.button, + clickCount, + modifiers, + false, + 0.5, + Ci.nsIDOMWindowUtils.INPUT_SOURCE_MOUSE, + false ); } catch (e) { - console.error("Error dispatching mouse event:", e); + console.error('Error dispatching mouse event:', e); } } @@ -66,9 +73,14 @@ export class ZenEdgeScrollChild extends JSWindowActorChild { } const doc = contentWin.document; - - const clientX = typeof wheelData.clientX === 'number' ? wheelData.clientX : (doc.documentElement.clientWidth / 2); - const clientY = typeof wheelData.clientY === 'number' ? wheelData.clientY : (doc.documentElement.clientHeight / 2); + const clientX = + typeof wheelData.clientX === 'number' + ? wheelData.clientX + : doc.documentElement.clientWidth / 2; + const clientY = + typeof wheelData.clientY === 'number' + ? wheelData.clientY + : doc.documentElement.clientHeight / 2; let modifiers = 0; if (wheelData.altKey) modifiers |= Ci.nsIDOMWindowUtils.MODIFIER_ALT; @@ -78,15 +90,27 @@ export class ZenEdgeScrollChild extends JSWindowActorChild { try { contentWin.windowUtils.sendWheelEvent( - clientX, clientY, wheelData.deltaX, wheelData.deltaY, wheelData.deltaZ, - wheelData.deltaMode, modifiers, 0, 0, true, false, false, false, false + clientX, + clientY, + wheelData.deltaX, + wheelData.deltaY, + wheelData.deltaZ, + wheelData.deltaMode, + modifiers, + 0, + 0, + true, + false, + false, + false, + false ); } catch (e) { - console.error("Error dispatching wheel event:", e); + console.error('Error dispatching wheel event:', e); } } destroy() { super.destroy(); } -} \ No newline at end of file +} diff --git a/src/zen/edgescroll/actors/ZenEdgeScrollParent.sys.mjs b/src/zen/edgescroll/actors/ZenEdgeScrollParent.sys.mjs index b7ab588478..de66bc72f5 100644 --- a/src/zen/edgescroll/actors/ZenEdgeScrollParent.sys.mjs +++ b/src/zen/edgescroll/actors/ZenEdgeScrollParent.sys.mjs @@ -1,7 +1,5 @@ /* global Services */ // For linter -console.log("ZenEdgeScrollParent: " + "alive" + "\n"); - export class ZenEdgeScrollParent extends JSWindowActorParent { constructor() { super(); @@ -24,4 +22,4 @@ export class ZenEdgeScrollParent extends JSWindowActorParent { destroy() { super.destroy(); } -} \ No newline at end of file +} From ead77dcc26614e1d1fef02cdb50f854c9d5b3daa Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Fri, 23 May 2025 00:07:45 +0800 Subject: [PATCH 17/34] fix: Allow content window zoom --- src/zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs b/src/zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs index f013d5245a..7891933261 100644 --- a/src/zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs +++ b/src/zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs @@ -48,8 +48,8 @@ export class ZenEdgeScrollChild extends JSWindowActorChild { try { contentWin.windowUtils.sendMouseEvent( data.type, - data.clientX, - data.clientY, + data.clientX / contentWin.devicePixelRatio, + data.clientY / contentWin.devicePixelRatio, data.button, clickCount, modifiers, From 33162bddfaa6ea3453ce9dbd96538deb70e3d38c Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Fri, 23 May 2025 15:33:47 +0800 Subject: [PATCH 18/34] test: Add tests for ZenEdgeScroll --- src/zen/edgescroll/ZenEdgeScrollManager.mjs | 1 + src/zen/tests/edgescroll/browser.toml | 5 ++ .../edgescroll/browser_edgescroll_click.js | 60 ++++++++++++++ .../edgescroll/browser_edgescroll_drag.js | 83 +++++++++++++++++++ .../browser_edgescroll_triggerdiv.js | 38 +++++++++ .../edgescroll/browser_edgescroll_wheel.js | 67 +++++++++++++++ src/zen/tests/moz.build | 1 + 7 files changed, 255 insertions(+) create mode 100644 src/zen/tests/edgescroll/browser.toml create mode 100644 src/zen/tests/edgescroll/browser_edgescroll_click.js create mode 100644 src/zen/tests/edgescroll/browser_edgescroll_drag.js create mode 100644 src/zen/tests/edgescroll/browser_edgescroll_triggerdiv.js create mode 100644 src/zen/tests/edgescroll/browser_edgescroll_wheel.js diff --git a/src/zen/edgescroll/ZenEdgeScrollManager.mjs b/src/zen/edgescroll/ZenEdgeScrollManager.mjs index 728f90efe5..7f45bb065b 100644 --- a/src/zen/edgescroll/ZenEdgeScrollManager.mjs +++ b/src/zen/edgescroll/ZenEdgeScrollManager.mjs @@ -318,6 +318,7 @@ matches: [ '*://*/*', 'about:*', // For about: pages + 'data:*', // For testing purposes ], includeChrome: true, // <--- ENSURE THIS LINE IS PRESENT AND SET TO TRUE }; diff --git a/src/zen/tests/edgescroll/browser.toml b/src/zen/tests/edgescroll/browser.toml new file mode 100644 index 0000000000..2c451855a5 --- /dev/null +++ b/src/zen/tests/edgescroll/browser.toml @@ -0,0 +1,5 @@ +["browser_edgescroll_triggerdiv.js"] +["browser_edgescroll_wheel.js"] +["browser_edgescroll_click.js"] +["browser_edgescroll_drag.js"] + diff --git a/src/zen/tests/edgescroll/browser_edgescroll_click.js b/src/zen/tests/edgescroll/browser_edgescroll_click.js new file mode 100644 index 0000000000..b7083bb7da --- /dev/null +++ b/src/zen/tests/edgescroll/browser_edgescroll_click.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +'use strict'; + +// Globals defined by the mochitest environment: +/* global TestUtils, Services, SpecialPowers, BrowserTestUtils, gBrowser, window, document, MouseEvent, info, ok, is */ + +add_task(async function test_ZenEdgeScroll_ClickScrollsContent() { + await SpecialPowers.pushPrefEnv({ set: [['zen.edgescroll.enabled', true]] }); + const tallPage = ` + data:text/html, + + + + + + + + `; + await BrowserTestUtils.openNewForegroundTab(window.gBrowser, tallPage, true); + + // wait for trigger + const trigger = await TestUtils.waitForCondition( + () => document.getElementById('zen-edge-scroll-trigger'), + 'Edge scroll trigger appears' + ); + + const browser = window.gBrowser.selectedBrowser; + await SpecialPowers.spawn(browser, [], () => content.scrollTo(0, 0)); + const initialScroll = await SpecialPowers.spawn(browser, [], () => content.scrollY); + + // simulate click on the edge trigger + const rect = trigger.getBoundingClientRect(); + const clientX = rect.left + rect.width / 2; + const clientY = rect.top + rect.height / 2; + // press down on the trigger + trigger.dispatchEvent(new MouseEvent('mousedown', { + clientX, + clientY, + bubbles: true, + cancelable: true, + })); + // release to complete the click + document.dispatchEvent(new MouseEvent('mouseup', { + clientX, + clientY, + bubbles: true, + cancelable: true, + })); + + // wait for synthetic scroll + await new Promise(r => setTimeout(r, 500)); + const newScroll = await SpecialPowers.spawn(browser, [], () => content.scrollY); + ok(newScroll > initialScroll, 'Clicking the edge trigger scrolls the content'); + + BrowserTestUtils.removeTab(window.gBrowser.selectedTab); +}); \ No newline at end of file diff --git a/src/zen/tests/edgescroll/browser_edgescroll_drag.js b/src/zen/tests/edgescroll/browser_edgescroll_drag.js new file mode 100644 index 0000000000..a796b7d9e2 --- /dev/null +++ b/src/zen/tests/edgescroll/browser_edgescroll_drag.js @@ -0,0 +1,83 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +'use strict'; + +// Globals defined by the mochitest environment: +/* global TestUtils, Services, SpecialPowers, BrowserTestUtils, gBrowser, window, document, MouseEvent, info, ok, is */ + +add_task(async function test_ZenEdgeScroll_DragScrollsContent() { + await SpecialPowers.pushPrefEnv({ set: [['zen.edgescroll.enabled', true]] }); + const tallPage = ` + data:text/html, + + + + + + + + `; + await BrowserTestUtils.openNewForegroundTab(window.gBrowser, tallPage, true); + + // wait for trigger + const trigger = await TestUtils.waitForCondition( + () => document.getElementById('zen-edge-scroll-trigger'), + 'Edge scroll trigger appears' + ); + + const browser = window.gBrowser.selectedBrowser; + await SpecialPowers.spawn(browser, [], () => content.scrollTo(0, 0)); + const initialScroll = await SpecialPowers.spawn(browser, [], () => content.scrollY); + + // simulate click on the edge trigger + const rect = trigger.getBoundingClientRect(); + const clientX = rect.left + rect.width / 2; + const startY = rect.top + rect.height / 2; + const endY = startY + 200; + const steps = 10; + const deltaY = (endY - startY) / steps; + + // press down on the trigger + trigger.dispatchEvent( + new MouseEvent('mousedown', { + clientX, + clientY: startY, + bubbles: true, + cancelable: true, + }) + ); + + // gradually move in small steps + for (let i = 1; i <= steps; i++) { + document.dispatchEvent( + new MouseEvent('mousemove', { + clientX, + clientY: startY + deltaY * i, + bubbles: true, + cancelable: true, + }) + ); + // small delay between moves to simulate a slow drag + await new Promise((r) => setTimeout(r, 200)); + } + + // release to complete the drag + document.dispatchEvent( + new MouseEvent('mouseup', { + clientX, + clientY: endY, + bubbles: true, + cancelable: true, + }) + ); + + // wait for synthetic scroll + await new Promise((r) => setTimeout(r, 500)); + const newScroll = await SpecialPowers.spawn(browser, [], () => content.scrollY); + ok(newScroll > initialScroll, 'Clicking the edge trigger scrolls the content'); + + BrowserTestUtils.removeTab(window.gBrowser.selectedTab); +}); diff --git a/src/zen/tests/edgescroll/browser_edgescroll_triggerdiv.js b/src/zen/tests/edgescroll/browser_edgescroll_triggerdiv.js new file mode 100644 index 0000000000..8193e796a8 --- /dev/null +++ b/src/zen/tests/edgescroll/browser_edgescroll_triggerdiv.js @@ -0,0 +1,38 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +'use strict'; + +// Globals defined by the mochitest environment: +/* global TestUtils, Services, SpecialPowers, BrowserTestUtils, gBrowser, window, document, MouseEvent, info, ok, is */ + +add_task(async function test_ZenEdgeScroll_TriggerExists() { + await SpecialPowers.pushPrefEnv({ set: [['zen.edgescroll.enabled', true]] }); + // Open a simple page to initialize the edge-scroll manager + await BrowserTestUtils.openNewForegroundTab( + window.gBrowser, + ` + data:text/html, + + + + + + + + `, + true + ); + + // Wait for the trigger div to appear + await TestUtils.waitForCondition( + () => !!document.getElementById('zen-edge-scroll-trigger'), + 'Edge scroll trigger div should be created' + ); + const trigger = document.getElementById('zen-edge-scroll-trigger'); + ok(trigger, 'The zen-edge-scroll-trigger div exists'); + + BrowserTestUtils.removeTab(window.gBrowser.selectedTab); +}); diff --git a/src/zen/tests/edgescroll/browser_edgescroll_wheel.js b/src/zen/tests/edgescroll/browser_edgescroll_wheel.js new file mode 100644 index 0000000000..df28f073e5 --- /dev/null +++ b/src/zen/tests/edgescroll/browser_edgescroll_wheel.js @@ -0,0 +1,67 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +'use strict'; + +// Globals defined by the mochitest environment: +/* global TestUtils, Services, SpecialPowers, BrowserTestUtils, gBrowser, window, document, MouseEvent, info, ok, is */ + +add_task(async function test_ZenEdgeScroll_WheelScrollsContent() { + await SpecialPowers.pushPrefEnv({ set: [['zen.edgescroll.enabled', true]] }); + // add so scrolling actually works + const tallPage = ` + data:text/html, + + + + + + + + `; + await BrowserTestUtils.openNewForegroundTab(window.gBrowser, tallPage, true); + + // give the trigger a moment + await new Promise((r) => setTimeout(r, 100)); + + const trigger = await TestUtils.waitForCondition( + () => document.getElementById('zen-edge-scroll-trigger'), + 'Edge scroll trigger appears' + ); + + const browser = window.gBrowser.selectedBrowser; + await SpecialPowers.spawn(browser, [], () => content.scrollTo(0, 0)); + const initialScroll = await SpecialPowers.spawn( + browser, + [], + () => content.document.documentElement.scrollTop + ); + + const rect = trigger.getBoundingClientRect(); + const wheelEvent = new WheelEvent('wheel', { + deltaY: 200, + clientY: rect.top + 20, + bubbles: true, + cancelable: true, + }); + trigger.dispatchEvent(wheelEvent); + + // give it time to scroll + await new Promise((r) => setTimeout(r, 500)); + + const newScroll = await SpecialPowers.spawn( + browser, + [], + () => content.document.documentElement.scrollTop + ); + info('Initial scroll:', initialScroll); + info('New scroll:', newScroll); + ok( + newScroll > initialScroll, + 'Content should scroll when wheel event is dispatched to the edge trigger' + ); + + BrowserTestUtils.removeTab(window.gBrowser.selectedTab); +}); diff --git a/src/zen/tests/moz.build b/src/zen/tests/moz.build index 26051fed00..578f2b6c8e 100644 --- a/src/zen/tests/moz.build +++ b/src/zen/tests/moz.build @@ -2,6 +2,7 @@ BROWSER_CHROME_MANIFESTS += [ "compact_mode/browser.toml", "container_essentials/browser.toml", + "edgescroll/browser.toml", "glance/browser.toml", "pinned/browser.toml", "urlbar/browser.toml", From 83cd9807793e7729d1836e413ead7f071fe259b5 Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Fri, 23 May 2025 15:46:47 +0800 Subject: [PATCH 19/34] fix: Renamed var to gZenEdgeScrollManager and make _initialized as property --- src/zen/edgescroll/ZenEdgeScrollManager.mjs | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/zen/edgescroll/ZenEdgeScrollManager.mjs b/src/zen/edgescroll/ZenEdgeScrollManager.mjs index 7f45bb065b..137dc49966 100644 --- a/src/zen/edgescroll/ZenEdgeScrollManager.mjs +++ b/src/zen/edgescroll/ZenEdgeScrollManager.mjs @@ -17,11 +17,13 @@ this._boundHandleSyntheticDragEnd = this.handleSyntheticDragEnd.bind(this); this._boundHandleWheel = this.handleWheel.bind(this); this._boundUpdateTriggerDivDisplay = this._updateTriggerDivDisplay.bind(this); // Added + this._initialized = false; - if (window.gZenEdgeScrollManagerInitialized) { + if (window.gZenEdgeScrollManager._initialized === true) { + console.warn('ZenEdgeScrollManager is already initialized.'); return; } - window.gZenEdgeScrollManagerInitialized = true; + window.gZenEdgeScrollManager._initialized = true; // Create and append the edge scroll trigger div this.edgeScrollTriggerDiv = window.document.createElement('div'); @@ -71,7 +73,7 @@ 'zen.tabs.vertical.right-side', this._boundUpdateTriggerDivDisplay ); // Added: Remove observer - window.gZenEdgeScrollManagerInitialized = false; + window.gZenEdgeScrollManager._initialized = false; } _updateTriggerDivDisplay() { @@ -305,7 +307,7 @@ // This is modeled after ZenGlanceManager's registerWindowActors function registerEdgeScrollActors() { if (Services.prefs.getBoolPref('zen.edgescroll.enabled', true)) { - window.gZenEdgeScrollManagerInstance = new ZenEdgeScrollManager(); + window.gZenEdgeScrollManager = new ZenEdgeScrollManager(); const actorConfig = { parent: { @@ -318,7 +320,7 @@ matches: [ '*://*/*', 'about:*', // For about: pages - 'data:*', // For testing purposes + 'data:*', // For testing purposes ], includeChrome: true, // <--- ENSURE THIS LINE IS PRESENT AND SET TO TRUE }; From e63ca21e4957be7ce781ccd1e1fb8454adb284a7 Mon Sep 17 00:00:00 2001 From: "mr. m" <91018726+mauro-balades@users.noreply.github.com> Date: Fri, 23 May 2025 20:02:32 +0200 Subject: [PATCH 20/34] Discard changes to src/browser/components/preferences/zen-settings.js --- src/browser/components/preferences/zen-settings.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/browser/components/preferences/zen-settings.js b/src/browser/components/preferences/zen-settings.js index 098fc2faaf..512d2e91d0 100644 --- a/src/browser/components/preferences/zen-settings.js +++ b/src/browser/components/preferences/zen-settings.js @@ -777,7 +777,6 @@ var gZenWorkspacesSettings = { }; Services.prefs.addObserver('zen.tab-unloader.enabled', tabsUnloaderPrefListener); Services.prefs.addObserver('zen.glance.enabled', tabsUnloaderPrefListener); // We can use the same listener for both prefs - Services.prefs.addObserver('zen.edgescroll.enabled', tabsUnloaderPrefListener); Services.prefs.addObserver( 'zen.workspaces.container-specific-essentials-enabled', tabsUnloaderPrefListener @@ -786,7 +785,6 @@ var gZenWorkspacesSettings = { window.addEventListener('unload', () => { Services.prefs.removeObserver('zen.tab-unloader.enabled', tabsUnloaderPrefListener); Services.prefs.removeObserver('zen.glance.enabled', tabsUnloaderPrefListener); - Services.prefs.removeObserver('zen.edgescroll.enabled', tabsUnloaderPrefListener); Services.prefs.removeObserver('zen.glance.activation-method', tabsUnloaderPrefListener); Services.prefs.removeObserver( 'zen.workspaces.container-specific-essentials-enabled', @@ -1171,11 +1169,6 @@ Preferences.addAll([ type: 'bool', default: true, }, - { - id: 'zen.edgescroll.enabled', - type: 'bool', - default: true, - }, { id: 'zen.theme.color-prefs.use-workspace-colors', type: 'bool', From 00145da094ba54875c8a910c95bdd0db2d425613 Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Sat, 24 May 2025 14:30:40 +0800 Subject: [PATCH 21/34] fix: Move edgescroll style to css --- src/browser/base/content/zen-assets.inc.xhtml | 1 + .../base/content/zen-assets.jar.inc.mn | 1 + src/zen/edgescroll/ZenEdgeScrollManager.mjs | 24 +++++++++---------- src/zen/edgescroll/zen-edgescroll.css | 15 ++++++++++++ .../edgescroll/browser_edgescroll_click.js | 2 +- .../edgescroll/browser_edgescroll_drag.js | 2 +- .../browser_edgescroll_triggerdiv.js | 6 ++--- .../edgescroll/browser_edgescroll_wheel.js | 2 +- 8 files changed, 35 insertions(+), 18 deletions(-) create mode 100644 src/zen/edgescroll/zen-edgescroll.css diff --git a/src/browser/base/content/zen-assets.inc.xhtml b/src/browser/base/content/zen-assets.inc.xhtml index 7032c5ca66..d42878b6a3 100644 --- a/src/browser/base/content/zen-assets.inc.xhtml +++ b/src/browser/base/content/zen-assets.inc.xhtml @@ -19,6 +19,7 @@ + diff --git a/src/browser/base/content/zen-assets.jar.inc.mn b/src/browser/base/content/zen-assets.jar.inc.mn index 76a7005106..c4144a178a 100644 --- a/src/browser/base/content/zen-assets.jar.inc.mn +++ b/src/browser/base/content/zen-assets.jar.inc.mn @@ -62,6 +62,7 @@ content/browser/zen-components/actors/ZenGlanceParent.sys.mjs (../../zen/glance/actors/ZenGlanceParent.sys.mjs) content/browser/zen-components/ZenEdgeScrollManager.mjs (../../zen/edgescroll/ZenEdgeScrollManager.mjs) + content/browser/zen-styles/zen-edgescroll.css (../../zen/edgescroll/zen-edgescroll.css) content/browser/zen-components/actors/ZenEdgeScrollChild.sys.mjs (../../zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs) content/browser/zen-components/actors/ZenEdgeScrollParent.sys.mjs (../../zen/edgescroll/actors/ZenEdgeScrollParent.sys.mjs) diff --git a/src/zen/edgescroll/ZenEdgeScrollManager.mjs b/src/zen/edgescroll/ZenEdgeScrollManager.mjs index 137dc49966..f95b46b718 100644 --- a/src/zen/edgescroll/ZenEdgeScrollManager.mjs +++ b/src/zen/edgescroll/ZenEdgeScrollManager.mjs @@ -1,5 +1,5 @@ { - const EDGE_INTERACTION_WIDTH_PX = Services.prefs.getIntPref('zen.theme.border-radius', 8); + const EDGE_INTERACTION_WIDTH_PX = Services.prefs.getIntPref('zen.theme.content-element-separation', 8); const SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE = 2; const ACTOR_NAME = 'ZenEdgeScroll'; // Name used for actor registration @@ -27,17 +27,17 @@ // Create and append the edge scroll trigger div this.edgeScrollTriggerDiv = window.document.createElement('div'); - this.edgeScrollTriggerDiv.id = 'zen-edge-scroll-trigger'; - Object.assign(this.edgeScrollTriggerDiv.style, { - position: 'fixed', - top: '0px', - right: '0px', - width: `${EDGE_INTERACTION_WIDTH_PX}px`, - height: '100%', - zIndex: '2147483647', // Max z-index - userSelect: 'none', - // backgroundColor: "rgba(255,0,0,0.1)", // For debugging visibility - }); + this.edgeScrollTriggerDiv.id = 'zen-edgescroll-trigger'; + // Object.assign(this.edgeScrollTriggerDiv.style, { + // position: 'fixed', + // top: '0px', + // right: '0px', + // width: `${EDGE_INTERACTION_WIDTH_PX}px`, + // height: '100%', + // zIndex: '2147483647', // Max z-index + // userSelect: 'none', + // // backgroundColor: "rgba(255,0,0,0.1)", // For debugging visibility + // }); window.document.documentElement.appendChild(this.edgeScrollTriggerDiv); this.edgeScrollTriggerDiv.addEventListener('mousedown', this._boundHandleMouseDown, true); diff --git a/src/zen/edgescroll/zen-edgescroll.css b/src/zen/edgescroll/zen-edgescroll.css new file mode 100644 index 0000000000..deb2fdef53 --- /dev/null +++ b/src/zen/edgescroll/zen-edgescroll.css @@ -0,0 +1,15 @@ +/* + * 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/. + */ + +#zen-edgescroll-trigger { + position: fixed; + top: 0px; + right: 0px; + width:var(--zen-element-separation); + height: 100%; + z-index: 2147483647; + user-select: none; +} diff --git a/src/zen/tests/edgescroll/browser_edgescroll_click.js b/src/zen/tests/edgescroll/browser_edgescroll_click.js index b7083bb7da..107c45abd5 100644 --- a/src/zen/tests/edgescroll/browser_edgescroll_click.js +++ b/src/zen/tests/edgescroll/browser_edgescroll_click.js @@ -24,7 +24,7 @@ add_task(async function test_ZenEdgeScroll_ClickScrollsContent() { // wait for trigger const trigger = await TestUtils.waitForCondition( - () => document.getElementById('zen-edge-scroll-trigger'), + () => document.getElementById('zen-edgescroll-trigger'), 'Edge scroll trigger appears' ); diff --git a/src/zen/tests/edgescroll/browser_edgescroll_drag.js b/src/zen/tests/edgescroll/browser_edgescroll_drag.js index a796b7d9e2..8e1fb71c12 100644 --- a/src/zen/tests/edgescroll/browser_edgescroll_drag.js +++ b/src/zen/tests/edgescroll/browser_edgescroll_drag.js @@ -24,7 +24,7 @@ add_task(async function test_ZenEdgeScroll_DragScrollsContent() { // wait for trigger const trigger = await TestUtils.waitForCondition( - () => document.getElementById('zen-edge-scroll-trigger'), + () => document.getElementById('zen-edgescroll-trigger'), 'Edge scroll trigger appears' ); diff --git a/src/zen/tests/edgescroll/browser_edgescroll_triggerdiv.js b/src/zen/tests/edgescroll/browser_edgescroll_triggerdiv.js index 8193e796a8..a9813c6246 100644 --- a/src/zen/tests/edgescroll/browser_edgescroll_triggerdiv.js +++ b/src/zen/tests/edgescroll/browser_edgescroll_triggerdiv.js @@ -28,11 +28,11 @@ add_task(async function test_ZenEdgeScroll_TriggerExists() { // Wait for the trigger div to appear await TestUtils.waitForCondition( - () => !!document.getElementById('zen-edge-scroll-trigger'), + () => !!document.getElementById('zen-edgescroll-trigger'), 'Edge scroll trigger div should be created' ); - const trigger = document.getElementById('zen-edge-scroll-trigger'); - ok(trigger, 'The zen-edge-scroll-trigger div exists'); + const trigger = document.getElementById('zen-edgescroll-trigger'); + ok(trigger, 'The zen-edgescroll-trigger div exists'); BrowserTestUtils.removeTab(window.gBrowser.selectedTab); }); diff --git a/src/zen/tests/edgescroll/browser_edgescroll_wheel.js b/src/zen/tests/edgescroll/browser_edgescroll_wheel.js index df28f073e5..549cbf4809 100644 --- a/src/zen/tests/edgescroll/browser_edgescroll_wheel.js +++ b/src/zen/tests/edgescroll/browser_edgescroll_wheel.js @@ -27,7 +27,7 @@ add_task(async function test_ZenEdgeScroll_WheelScrollsContent() { await new Promise((r) => setTimeout(r, 100)); const trigger = await TestUtils.waitForCondition( - () => document.getElementById('zen-edge-scroll-trigger'), + () => document.getElementById('zen-edgescroll-trigger'), 'Edge scroll trigger appears' ); From 426b1d72e1102825f502dc4908820d6bad995fe8 Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Sat, 24 May 2025 14:38:02 +0800 Subject: [PATCH 22/34] fix: Move trigger to zen-appcontent-wrapper --- src/zen/edgescroll/ZenEdgeScrollManager.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zen/edgescroll/ZenEdgeScrollManager.mjs b/src/zen/edgescroll/ZenEdgeScrollManager.mjs index f95b46b718..07cabf2d7f 100644 --- a/src/zen/edgescroll/ZenEdgeScrollManager.mjs +++ b/src/zen/edgescroll/ZenEdgeScrollManager.mjs @@ -38,7 +38,7 @@ // userSelect: 'none', // // backgroundColor: "rgba(255,0,0,0.1)", // For debugging visibility // }); - window.document.documentElement.appendChild(this.edgeScrollTriggerDiv); + document.getElementById("zen-appcontent-wrapper").appendChild(this.edgeScrollTriggerDiv); this.edgeScrollTriggerDiv.addEventListener('mousedown', this._boundHandleMouseDown, true); this.edgeScrollTriggerDiv.addEventListener('wheel', this._boundHandleWheel, { From ea336645ee828a9bd672818aa001df11eaa35870 Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Sat, 24 May 2025 14:41:23 +0800 Subject: [PATCH 23/34] fix: Removed _initialized --- src/zen/edgescroll/ZenEdgeScrollManager.mjs | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/src/zen/edgescroll/ZenEdgeScrollManager.mjs b/src/zen/edgescroll/ZenEdgeScrollManager.mjs index 07cabf2d7f..264b93a505 100644 --- a/src/zen/edgescroll/ZenEdgeScrollManager.mjs +++ b/src/zen/edgescroll/ZenEdgeScrollManager.mjs @@ -17,27 +17,15 @@ this._boundHandleSyntheticDragEnd = this.handleSyntheticDragEnd.bind(this); this._boundHandleWheel = this.handleWheel.bind(this); this._boundUpdateTriggerDivDisplay = this._updateTriggerDivDisplay.bind(this); // Added - this._initialized = false; - if (window.gZenEdgeScrollManager._initialized === true) { + if (this.edgeScrollTriggerDiv !== null) { console.warn('ZenEdgeScrollManager is already initialized.'); return; } - window.gZenEdgeScrollManager._initialized = true; // Create and append the edge scroll trigger div this.edgeScrollTriggerDiv = window.document.createElement('div'); this.edgeScrollTriggerDiv.id = 'zen-edgescroll-trigger'; - // Object.assign(this.edgeScrollTriggerDiv.style, { - // position: 'fixed', - // top: '0px', - // right: '0px', - // width: `${EDGE_INTERACTION_WIDTH_PX}px`, - // height: '100%', - // zIndex: '2147483647', // Max z-index - // userSelect: 'none', - // // backgroundColor: "rgba(255,0,0,0.1)", // For debugging visibility - // }); document.getElementById("zen-appcontent-wrapper").appendChild(this.edgeScrollTriggerDiv); this.edgeScrollTriggerDiv.addEventListener('mousedown', this._boundHandleMouseDown, true); @@ -73,7 +61,6 @@ 'zen.tabs.vertical.right-side', this._boundUpdateTriggerDivDisplay ); // Added: Remove observer - window.gZenEdgeScrollManager._initialized = false; } _updateTriggerDivDisplay() { From f6448d04ed5ab9dd9b092d5316576ca04db12d24 Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Sat, 24 May 2025 15:07:05 +0800 Subject: [PATCH 24/34] style: Formatting --- src/zen/edgescroll/ZenEdgeScrollManager.mjs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/zen/edgescroll/ZenEdgeScrollManager.mjs b/src/zen/edgescroll/ZenEdgeScrollManager.mjs index 264b93a505..0bf3c99bd5 100644 --- a/src/zen/edgescroll/ZenEdgeScrollManager.mjs +++ b/src/zen/edgescroll/ZenEdgeScrollManager.mjs @@ -1,5 +1,8 @@ { - const EDGE_INTERACTION_WIDTH_PX = Services.prefs.getIntPref('zen.theme.content-element-separation', 8); + const EDGE_INTERACTION_WIDTH_PX = Services.prefs.getIntPref( + 'zen.theme.content-element-separation', + 8 + ); const SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE = 2; const ACTOR_NAME = 'ZenEdgeScroll'; // Name used for actor registration @@ -26,7 +29,7 @@ // Create and append the edge scroll trigger div this.edgeScrollTriggerDiv = window.document.createElement('div'); this.edgeScrollTriggerDiv.id = 'zen-edgescroll-trigger'; - document.getElementById("zen-appcontent-wrapper").appendChild(this.edgeScrollTriggerDiv); + document.getElementById('zen-appcontent-wrapper').appendChild(this.edgeScrollTriggerDiv); this.edgeScrollTriggerDiv.addEventListener('mousedown', this._boundHandleMouseDown, true); this.edgeScrollTriggerDiv.addEventListener('wheel', this._boundHandleWheel, { From 83056accad9c41cc8f839ea525dfc8bdbe93bb30 Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Sat, 24 May 2025 15:07:56 +0800 Subject: [PATCH 25/34] feat: Add observer for preference update --- src/zen/edgescroll/ZenEdgeScrollManager.mjs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/zen/edgescroll/ZenEdgeScrollManager.mjs b/src/zen/edgescroll/ZenEdgeScrollManager.mjs index 0bf3c99bd5..44bb0cf463 100644 --- a/src/zen/edgescroll/ZenEdgeScrollManager.mjs +++ b/src/zen/edgescroll/ZenEdgeScrollManager.mjs @@ -327,4 +327,22 @@ } registerEdgeScrollActors(); + + // Observe changes to the enabled pref and register/destroy the manager + const edgeScrollPrefObserver = { + observe(subject, topic, data) { + if (topic === 'nsPref:changed' && data === 'zen.edgescroll.enabled') { + if (window.gZenEdgeScrollManager) { + window.gZenEdgeScrollManager.destroy(); + window.gZenEdgeScrollManager = null; + } + if (Services.prefs.getBoolPref('zen.edgescroll.enabled', true)) { + registerEdgeScrollActors(); + if (window.gZenEdgeScrollManager) window.gZenEdgeScrollManager.init(); + } + } + }, + }; + + Services.prefs.addObserver('zen.edgescroll.enabled', edgeScrollPrefObserver, false); } From 564f7e31e67b660430a43a9805d6600240563069 Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Sun, 25 May 2025 14:31:19 +0800 Subject: [PATCH 26/34] fix: Rename id to specify vertical trigger --- src/zen/edgescroll/ZenEdgeScrollManager.mjs | 2 +- src/zen/edgescroll/zen-edgescroll.css | 2 +- src/zen/tests/edgescroll/browser_edgescroll_click.js | 2 +- src/zen/tests/edgescroll/browser_edgescroll_drag.js | 2 +- src/zen/tests/edgescroll/browser_edgescroll_triggerdiv.js | 6 +++--- src/zen/tests/edgescroll/browser_edgescroll_wheel.js | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/zen/edgescroll/ZenEdgeScrollManager.mjs b/src/zen/edgescroll/ZenEdgeScrollManager.mjs index 44bb0cf463..9afd6e7f03 100644 --- a/src/zen/edgescroll/ZenEdgeScrollManager.mjs +++ b/src/zen/edgescroll/ZenEdgeScrollManager.mjs @@ -28,7 +28,7 @@ // Create and append the edge scroll trigger div this.edgeScrollTriggerDiv = window.document.createElement('div'); - this.edgeScrollTriggerDiv.id = 'zen-edgescroll-trigger'; + this.edgeScrollTriggerDiv.id = 'zen-edgescroll-trigger-vertical'; document.getElementById('zen-appcontent-wrapper').appendChild(this.edgeScrollTriggerDiv); this.edgeScrollTriggerDiv.addEventListener('mousedown', this._boundHandleMouseDown, true); diff --git a/src/zen/edgescroll/zen-edgescroll.css b/src/zen/edgescroll/zen-edgescroll.css index deb2fdef53..bf20a65720 100644 --- a/src/zen/edgescroll/zen-edgescroll.css +++ b/src/zen/edgescroll/zen-edgescroll.css @@ -4,7 +4,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -#zen-edgescroll-trigger { +#zen-edgescroll-trigger-vertical { position: fixed; top: 0px; right: 0px; diff --git a/src/zen/tests/edgescroll/browser_edgescroll_click.js b/src/zen/tests/edgescroll/browser_edgescroll_click.js index 107c45abd5..70bff9a80e 100644 --- a/src/zen/tests/edgescroll/browser_edgescroll_click.js +++ b/src/zen/tests/edgescroll/browser_edgescroll_click.js @@ -24,7 +24,7 @@ add_task(async function test_ZenEdgeScroll_ClickScrollsContent() { // wait for trigger const trigger = await TestUtils.waitForCondition( - () => document.getElementById('zen-edgescroll-trigger'), + () => document.getElementById('zen-edgescroll-trigger-vertical'), 'Edge scroll trigger appears' ); diff --git a/src/zen/tests/edgescroll/browser_edgescroll_drag.js b/src/zen/tests/edgescroll/browser_edgescroll_drag.js index 8e1fb71c12..b0cdfb8382 100644 --- a/src/zen/tests/edgescroll/browser_edgescroll_drag.js +++ b/src/zen/tests/edgescroll/browser_edgescroll_drag.js @@ -24,7 +24,7 @@ add_task(async function test_ZenEdgeScroll_DragScrollsContent() { // wait for trigger const trigger = await TestUtils.waitForCondition( - () => document.getElementById('zen-edgescroll-trigger'), + () => document.getElementById('zen-edgescroll-trigger-vertical'), 'Edge scroll trigger appears' ); diff --git a/src/zen/tests/edgescroll/browser_edgescroll_triggerdiv.js b/src/zen/tests/edgescroll/browser_edgescroll_triggerdiv.js index a9813c6246..294829da18 100644 --- a/src/zen/tests/edgescroll/browser_edgescroll_triggerdiv.js +++ b/src/zen/tests/edgescroll/browser_edgescroll_triggerdiv.js @@ -28,11 +28,11 @@ add_task(async function test_ZenEdgeScroll_TriggerExists() { // Wait for the trigger div to appear await TestUtils.waitForCondition( - () => !!document.getElementById('zen-edgescroll-trigger'), + () => !!document.getElementById('zen-edgescroll-trigger-vertical'), 'Edge scroll trigger div should be created' ); - const trigger = document.getElementById('zen-edgescroll-trigger'); - ok(trigger, 'The zen-edgescroll-trigger div exists'); + const trigger = document.getElementById('zen-edgescroll-trigger-vertical'); + ok(trigger, 'The zen-edgescroll-trigger-vertical div exists'); BrowserTestUtils.removeTab(window.gBrowser.selectedTab); }); diff --git a/src/zen/tests/edgescroll/browser_edgescroll_wheel.js b/src/zen/tests/edgescroll/browser_edgescroll_wheel.js index 549cbf4809..02d18852fd 100644 --- a/src/zen/tests/edgescroll/browser_edgescroll_wheel.js +++ b/src/zen/tests/edgescroll/browser_edgescroll_wheel.js @@ -27,7 +27,7 @@ add_task(async function test_ZenEdgeScroll_WheelScrollsContent() { await new Promise((r) => setTimeout(r, 100)); const trigger = await TestUtils.waitForCondition( - () => document.getElementById('zen-edgescroll-trigger'), + () => document.getElementById('zen-edgescroll-trigger-vertical'), 'Edge scroll trigger appears' ); From 460b9795552360405f7d30c116be9d63db45172e Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Sun, 25 May 2025 14:39:21 +0800 Subject: [PATCH 27/34] fix: Modified logic for synthetic event --- src/zen/edgescroll/ZenEdgeScrollManager.mjs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/zen/edgescroll/ZenEdgeScrollManager.mjs b/src/zen/edgescroll/ZenEdgeScrollManager.mjs index 9afd6e7f03..708f4b4827 100644 --- a/src/zen/edgescroll/ZenEdgeScrollManager.mjs +++ b/src/zen/edgescroll/ZenEdgeScrollManager.mjs @@ -126,7 +126,10 @@ createSyntheticEventData(originalEvent, targetBrowserRect, eventType) { const clientXInContent = Math.max( 0, - Math.floor(targetBrowserRect.width - SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE) + Math.min( + Math.floor(originalEvent.clientX - targetBrowserRect.left), + Math.floor(targetBrowserRect.width - SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE) + ) ); const clientYInContent = Math.max( 0, From bc9cbab92411627a6c9018edb30c11d45916314c Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Sun, 25 May 2025 14:51:20 +0800 Subject: [PATCH 28/34] fix: Specify edge scroll direction --- src/zen/edgescroll/ZenEdgeScrollManager.mjs | 80 +++++++++++---------- 1 file changed, 43 insertions(+), 37 deletions(-) diff --git a/src/zen/edgescroll/ZenEdgeScrollManager.mjs b/src/zen/edgescroll/ZenEdgeScrollManager.mjs index 708f4b4827..fda679600a 100644 --- a/src/zen/edgescroll/ZenEdgeScrollManager.mjs +++ b/src/zen/edgescroll/ZenEdgeScrollManager.mjs @@ -13,7 +13,7 @@ targetBrowserDuringDrag: null, targetBrowsingContextDuringDrag: null, }; - this.edgeScrollTriggerDiv = null; // Added for the trigger div + this.triggerDivVertical = null; // Added for the trigger div this._boundHandleMouseDown = this.handleMouseDown.bind(this); this._boundHandleSyntheticDrag = this.handleSyntheticDrag.bind(this); @@ -21,18 +21,21 @@ this._boundHandleWheel = this.handleWheel.bind(this); this._boundUpdateTriggerDivDisplay = this._updateTriggerDivDisplay.bind(this); // Added - if (this.edgeScrollTriggerDiv !== null) { + if (this.triggerDivVertical !== null) { console.warn('ZenEdgeScrollManager is already initialized.'); return; } // Create and append the edge scroll trigger div - this.edgeScrollTriggerDiv = window.document.createElement('div'); - this.edgeScrollTriggerDiv.id = 'zen-edgescroll-trigger-vertical'; - document.getElementById('zen-appcontent-wrapper').appendChild(this.edgeScrollTriggerDiv); + this.triggerDivVertical = window.document.createElement('div'); + this.triggerDivVertical.id = 'zen-edgescroll-trigger-vertical'; + // add attribute vertical to the trigger div + this.triggerDivVertical.setAttribute('direction', 'vertical'); - this.edgeScrollTriggerDiv.addEventListener('mousedown', this._boundHandleMouseDown, true); - this.edgeScrollTriggerDiv.addEventListener('wheel', this._boundHandleWheel, { + document.getElementById('zen-appcontent-wrapper').appendChild(this.triggerDivVertical); + + this.triggerDivVertical.addEventListener('mousedown', this._boundHandleMouseDown, true); + this.triggerDivVertical.addEventListener('wheel', this._boundHandleWheel, { capture: true, passive: false, }); @@ -45,19 +48,19 @@ } destroy() { - if (this.edgeScrollTriggerDiv) { - this.edgeScrollTriggerDiv.removeEventListener( + if (this.triggerDivVertical) { + this.triggerDivVertical.removeEventListener( 'mousedown', this._boundHandleMouseDown, true ); - this.edgeScrollTriggerDiv.removeEventListener('wheel', this._boundHandleWheel, true); - if (this.edgeScrollTriggerDiv.parentNode) { - this.edgeScrollTriggerDiv.parentNode.removeChild(this.edgeScrollTriggerDiv); // Corrected removeChild call + this.triggerDivVertical.removeEventListener('wheel', this._boundHandleWheel, true); + if (this.triggerDivVertical.parentNode) { + this.triggerDivVertical.parentNode.removeChild(this.triggerDivVertical); // Corrected removeChild call } - this.edgeScrollTriggerDiv = null; + this.triggerDivVertical = null; } - // These listeners are added to window, not edgeScrollTriggerDiv in handleMouseDown + // These listeners are added to window, not triggerDivVertical in handleMouseDown window.removeEventListener('mousemove', this._boundHandleSyntheticDrag, true); window.removeEventListener('mouseup', this._boundHandleSyntheticDragEnd, true); Services.prefs.removeObserver( @@ -68,13 +71,13 @@ _updateTriggerDivDisplay() { // Added method - if (!this.edgeScrollTriggerDiv) { + if (!this.triggerDivVertical) { return; } if (window.gZenCompactModeManager && gZenCompactModeManager.sidebarIsOnRight) { - this.edgeScrollTriggerDiv.style.display = 'none'; + this.triggerDivVertical.style.display = 'none'; } else { - this.edgeScrollTriggerDiv.style.display = 'block'; + this.triggerDivVertical.style.display = 'block'; } } @@ -100,27 +103,30 @@ let potentialTargetBrowserRect = null; const selectedBrowser = gBrowser.selectedBrowser; - if (selectedBrowser && selectedBrowser.getAttribute('primary') === 'true') { - const selectedBrowserRect = selectedBrowser.getBoundingClientRect(); - if (selectedBrowserRect.width > 0 && selectedBrowserRect.height > 0) { - // Check if the browser's right edge is very close to the window's right edge - const isBrowserAtRightEdge = - windowWidth - selectedBrowserRect.right <= EDGE_INTERACTION_WIDTH_PX + 1; - const isEventYWithinBrowser = - eventClientY >= selectedBrowserRect.top && eventClientY <= selectedBrowserRect.bottom; - if (isBrowserAtRightEdge && isEventYWithinBrowser) { - potentialTargetBrowser = selectedBrowser; - potentialTargetBrowserRect = selectedBrowserRect; + + if (event.target == this.triggerDivVertical) { + if (selectedBrowser && selectedBrowser.getAttribute('primary') === 'true') { + const selectedBrowserRect = selectedBrowser.getBoundingClientRect(); + if (selectedBrowserRect.width > 0 && selectedBrowserRect.height > 0) { + // Check if the browser's right edge is very close to the window's right edge + const isBrowserAtRightEdge = + windowWidth - selectedBrowserRect.right <= EDGE_INTERACTION_WIDTH_PX + 1; + const isEventYWithinBrowser = + eventClientY >= selectedBrowserRect.top && eventClientY <= selectedBrowserRect.bottom; + if (isBrowserAtRightEdge && isEventYWithinBrowser) { + potentialTargetBrowser = selectedBrowser; + potentialTargetBrowserRect = selectedBrowserRect; + } } } - } - // The event is on the trigger div, so it is "in gap". We return the browser found. - return { - isInGap: true, - targetBrowser: potentialTargetBrowser, - browserRect: potentialTargetBrowserRect, - }; + // The event is on the trigger div, so it is "in gap". We return the browser found. + return { + isInGap: true, + targetBrowser: potentialTargetBrowser, + browserRect: potentialTargetBrowserRect, + }; + } } createSyntheticEventData(originalEvent, targetBrowserRect, eventType) { @@ -158,7 +164,7 @@ handleMouseDown(event) { if (event.button !== 0) return; - const gapInfo = this.getGapZoneInfo(event); // event is from edgeScrollTriggerDiv + const gapInfo = this.getGapZoneInfo(event); // event is from triggerDivVertical if (!gapInfo.targetBrowser) { return; @@ -257,7 +263,7 @@ } handleWheel(event) { - const gapInfo = this.getGapZoneInfo(event); // event is from edgeScrollTriggerDiv + const gapInfo = this.getGapZoneInfo(event); // event is from triggerDivVertical if (!gapInfo.targetBrowser) { return; From b34da26a7dd682f818770736f9a32825f27b231c Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Sun, 25 May 2025 14:57:10 +0800 Subject: [PATCH 29/34] fix: Discard unused attribute --- src/zen/edgescroll/ZenEdgeScrollManager.mjs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/zen/edgescroll/ZenEdgeScrollManager.mjs b/src/zen/edgescroll/ZenEdgeScrollManager.mjs index fda679600a..cff2d44978 100644 --- a/src/zen/edgescroll/ZenEdgeScrollManager.mjs +++ b/src/zen/edgescroll/ZenEdgeScrollManager.mjs @@ -13,13 +13,14 @@ targetBrowserDuringDrag: null, targetBrowsingContextDuringDrag: null, }; - this.triggerDivVertical = null; // Added for the trigger div + + this.triggerDivVertical = null; this._boundHandleMouseDown = this.handleMouseDown.bind(this); this._boundHandleSyntheticDrag = this.handleSyntheticDrag.bind(this); this._boundHandleSyntheticDragEnd = this.handleSyntheticDragEnd.bind(this); this._boundHandleWheel = this.handleWheel.bind(this); - this._boundUpdateTriggerDivDisplay = this._updateTriggerDivDisplay.bind(this); // Added + this._boundUpdateTriggerDivDisplay = this._updateTriggerDivDisplay.bind(this); if (this.triggerDivVertical !== null) { console.warn('ZenEdgeScrollManager is already initialized.'); @@ -29,8 +30,6 @@ // Create and append the edge scroll trigger div this.triggerDivVertical = window.document.createElement('div'); this.triggerDivVertical.id = 'zen-edgescroll-trigger-vertical'; - // add attribute vertical to the trigger div - this.triggerDivVertical.setAttribute('direction', 'vertical'); document.getElementById('zen-appcontent-wrapper').appendChild(this.triggerDivVertical); From a7ac8a85379de3142d298040372b2c80e5385097 Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Sun, 25 May 2025 15:18:39 +0800 Subject: [PATCH 30/34] feat: Add horizontal trigger --- src/zen/edgescroll/ZenEdgeScrollManager.mjs | 70 ++++++++++++++------- src/zen/edgescroll/zen-edgescroll.css | 10 +++ 2 files changed, 59 insertions(+), 21 deletions(-) diff --git a/src/zen/edgescroll/ZenEdgeScrollManager.mjs b/src/zen/edgescroll/ZenEdgeScrollManager.mjs index cff2d44978..9c493d8855 100644 --- a/src/zen/edgescroll/ZenEdgeScrollManager.mjs +++ b/src/zen/edgescroll/ZenEdgeScrollManager.mjs @@ -4,6 +4,7 @@ 8 ); const SYNTHETIC_EVENT_X_OFFSET_FROM_RIGHT_EDGE = 2; + const SYNTHETIC_EVENT_Y_OFFSET_FROM_BOTTOM_EDGE = 2; const ACTOR_NAME = 'ZenEdgeScroll'; // Name used for actor registration class ZenEdgeScrollManager extends ZenDOMOperatedFeature { @@ -14,15 +15,16 @@ targetBrowsingContextDuringDrag: null, }; - this.triggerDivVertical = null; + this.triggerDivVertical = null; + this.triggerDivHorizontal = null; this._boundHandleMouseDown = this.handleMouseDown.bind(this); this._boundHandleSyntheticDrag = this.handleSyntheticDrag.bind(this); this._boundHandleSyntheticDragEnd = this.handleSyntheticDragEnd.bind(this); this._boundHandleWheel = this.handleWheel.bind(this); - this._boundUpdateTriggerDivDisplay = this._updateTriggerDivDisplay.bind(this); + this._boundUpdateTriggerDivDisplay = this._updateTriggerDivDisplay.bind(this); - if (this.triggerDivVertical !== null) { + if (this.triggerDivVertical !== null && this.triggerDivVertical !== null) { console.warn('ZenEdgeScrollManager is already initialized.'); return; } @@ -30,15 +32,22 @@ // Create and append the edge scroll trigger div this.triggerDivVertical = window.document.createElement('div'); this.triggerDivVertical.id = 'zen-edgescroll-trigger-vertical'; - document.getElementById('zen-appcontent-wrapper').appendChild(this.triggerDivVertical); - this.triggerDivVertical.addEventListener('mousedown', this._boundHandleMouseDown, true); this.triggerDivVertical.addEventListener('wheel', this._boundHandleWheel, { capture: true, passive: false, }); + this.triggerDivHorizontal = window.document.createElement('div'); + this.triggerDivHorizontal.id = 'zen-edgescroll-trigger-horizontal'; + document.getElementById('zen-appcontent-wrapper').appendChild(this.triggerDivHorizontal); + this.triggerDivHorizontal.addEventListener('mousedown', this._boundHandleMouseDown, true); + this.triggerDivHorizontal.addEventListener('wheel', this._boundHandleWheel, { + capture: true, + passive: false, + }); + this._updateTriggerDivDisplay(); // Added: Set initial display state Services.prefs.addObserver( 'zen.tabs.vertical.right-side', @@ -48,18 +57,26 @@ destroy() { if (this.triggerDivVertical) { - this.triggerDivVertical.removeEventListener( - 'mousedown', - this._boundHandleMouseDown, - true - ); + this.triggerDivVertical.removeEventListener('mousedown', this._boundHandleMouseDown, true); this.triggerDivVertical.removeEventListener('wheel', this._boundHandleWheel, true); if (this.triggerDivVertical.parentNode) { this.triggerDivVertical.parentNode.removeChild(this.triggerDivVertical); // Corrected removeChild call } this.triggerDivVertical = null; } - // These listeners are added to window, not triggerDivVertical in handleMouseDown + if (this.triggerDivHorizontal) { + this.triggerDivHorizontal.removeEventListener( + 'mousedown', + this._boundHandleMouseDown, + true + ); + this.triggerDivHorizontal.removeEventListener('wheel', this._boundHandleWheel, true); + if (this.triggerDivHorizontal.parentNode) { + this.triggerDivHorizontal.parentNode.removeChild(this.triggerDivHorizontal); // Corrected removeChild call + } + this.triggerDivHorizontal = null; + } + // These listeners are added to window, not triggerDiv in handleMouseDown window.removeEventListener('mousemove', this._boundHandleSyntheticDrag, true); window.removeEventListener('mouseup', this._boundHandleSyntheticDragEnd, true); Services.prefs.removeObserver( @@ -118,14 +135,25 @@ } } } - - // The event is on the trigger div, so it is "in gap". We return the browser found. - return { - isInGap: true, - targetBrowser: potentialTargetBrowser, - browserRect: potentialTargetBrowserRect, - }; - } + } else if (event.target == this.triggerDivHorizontal) { + // For horizontal trigger, we can check if the event is close to the bottom edge + const selectedBrowserRect = selectedBrowser.getBoundingClientRect(); + if (selectedBrowserRect.width > 0 && selectedBrowserRect.height > 0) { + const isBrowserAtBottomEdge = + window.innerHeight - selectedBrowserRect.bottom <= EDGE_INTERACTION_WIDTH_PX + 1; + const isEventXWithinBrowser = + event.clientX >= selectedBrowserRect.left && event.clientX <= selectedBrowserRect.right; + if (isBrowserAtBottomEdge && isEventXWithinBrowser) { + potentialTargetBrowser = selectedBrowser; + potentialTargetBrowserRect = selectedBrowserRect; + } + } + } + return { + isInGap: true, + targetBrowser: potentialTargetBrowser, + browserRect: potentialTargetBrowserRect, + }; } createSyntheticEventData(originalEvent, targetBrowserRect, eventType) { @@ -163,7 +191,7 @@ handleMouseDown(event) { if (event.button !== 0) return; - const gapInfo = this.getGapZoneInfo(event); // event is from triggerDivVertical + const gapInfo = this.getGapZoneInfo(event); if (!gapInfo.targetBrowser) { return; @@ -262,7 +290,7 @@ } handleWheel(event) { - const gapInfo = this.getGapZoneInfo(event); // event is from triggerDivVertical + const gapInfo = this.getGapZoneInfo(event); if (!gapInfo.targetBrowser) { return; diff --git a/src/zen/edgescroll/zen-edgescroll.css b/src/zen/edgescroll/zen-edgescroll.css index bf20a65720..a7a903cfc8 100644 --- a/src/zen/edgescroll/zen-edgescroll.css +++ b/src/zen/edgescroll/zen-edgescroll.css @@ -13,3 +13,13 @@ z-index: 2147483647; user-select: none; } + +#zen-edgescroll-trigger-horizontal { + position: fixed; + bottom: 0px; + left: 0px; + width: 100%; + height: var(--zen-element-separation); + z-index: 2147483647; + user-select: none; +} From 0816774ca1457fc027cfbd50f86b8d80872a4b2f Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Sun, 25 May 2025 15:19:45 +0800 Subject: [PATCH 31/34] fix: Change css position to absolute + make vertical on top --- src/zen/edgescroll/zen-edgescroll.css | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/zen/edgescroll/zen-edgescroll.css b/src/zen/edgescroll/zen-edgescroll.css index a7a903cfc8..07d5307e2b 100644 --- a/src/zen/edgescroll/zen-edgescroll.css +++ b/src/zen/edgescroll/zen-edgescroll.css @@ -5,7 +5,7 @@ */ #zen-edgescroll-trigger-vertical { - position: fixed; + position: absolute; top: 0px; right: 0px; width:var(--zen-element-separation); @@ -15,11 +15,11 @@ } #zen-edgescroll-trigger-horizontal { - position: fixed; + position: absolute; bottom: 0px; left: 0px; width: 100%; height: var(--zen-element-separation); - z-index: 2147483647; + z-index: 2147483646; user-select: none; } From 8ea7e051d49bb1db7db418ea861cd20a8710722a Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Sun, 25 May 2025 15:22:33 +0800 Subject: [PATCH 32/34] fix: Correct Y offset --- src/zen/edgescroll/ZenEdgeScrollManager.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/zen/edgescroll/ZenEdgeScrollManager.mjs b/src/zen/edgescroll/ZenEdgeScrollManager.mjs index 9c493d8855..85ffadf4b0 100644 --- a/src/zen/edgescroll/ZenEdgeScrollManager.mjs +++ b/src/zen/edgescroll/ZenEdgeScrollManager.mjs @@ -168,7 +168,7 @@ 0, Math.min( Math.floor(originalEvent.clientY - targetBrowserRect.top), - Math.floor(targetBrowserRect.height - 1) + Math.floor(targetBrowserRect.height - SYNTHETIC_EVENT_Y_OFFSET_FROM_BOTTOM_EDGE) ) ); const screenX = Math.floor(window.screenX + targetBrowserRect.left + clientXInContent); From 12c39bf988e3197cb76b8865f02362d0f3298a7f Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Fri, 18 Jul 2025 14:23:14 +0800 Subject: [PATCH 33/34] fix: move edgescroll.inc --- src/browser/app/profile/{ => features}/edgescroll.inc | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/browser/app/profile/{ => features}/edgescroll.inc (100%) diff --git a/src/browser/app/profile/edgescroll.inc b/src/browser/app/profile/features/edgescroll.inc similarity index 100% rename from src/browser/app/profile/edgescroll.inc rename to src/browser/app/profile/features/edgescroll.inc From 1252fe966f6495f0bf1a604c8f8aa0590c97eb67 Mon Sep 17 00:00:00 2001 From: Stefan Tanuwijaya Date: Fri, 18 Jul 2025 16:11:36 +0800 Subject: [PATCH 34/34] Update scrollwheel sendWheelEvent --- src/zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs b/src/zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs index 7891933261..54d14a8a01 100644 --- a/src/zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs +++ b/src/zen/edgescroll/actors/ZenEdgeScrollChild.sys.mjs @@ -99,11 +99,7 @@ export class ZenEdgeScrollChild extends JSWindowActorChild { modifiers, 0, 0, - true, - false, - false, - false, - false + 0 ); } catch (e) { console.error('Error dispatching wheel event:', e);