Skip to content

Commit

Permalink
🚧 WIP: Begin rework of context menu actors
Browse files Browse the repository at this point in the history
  • Loading branch information
kierandrewett committed Apr 19, 2024
1 parent 1763691 commit 5b6579b
Show file tree
Hide file tree
Showing 6 changed files with 237 additions and 66 deletions.
182 changes: 158 additions & 24 deletions actors/DotContextMenuChild.sys.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,47 +6,181 @@ const { ContentDOMReference } = ChromeUtils.importESModule(
"resource://gre/modules/ContentDOMReference.sys.mjs"
);

const { DOMUtils } = ChromeUtils.importESModule(
"resource://gre/modules/DOMUtils.sys.mjs"
);

const { BrowserCustomizableShared } = ChromeUtils.importESModule(
"resource://gre/modules/BrowserCustomizableShared.sys.mjs"
);

const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";

export class DotContextMenuChild extends JSWindowActorChild {
/**
* @param {import("third_party/dothq/gecko-types/lib").ReceiveMessageArgument} message
* Creates a new context object
* @param {Event} event
* @param {Node} target
*/
receiveMessage(message) {
const { targetIdentifier } = message.data;
createContext(event, target) {
return {};
}

/**
* Fired when a context menu is requested via the contextmenu DOM event
* @param {MouseEvent} event
*/
#onContextMenuRequest(event) {
let { defaultPrevented } = event;

const composedTarget = /** @type {Element} */ (event.composedTarget);

// Ignore contextmenu events on a chrome browser, as we'll
// handle the contextmenu event from inside the child content.
if (
composedTarget.namespaceURI == XUL_NS &&
composedTarget.tagName == "browser"
)
return;

if (
// If the event originated from a non-chrome document
// and we have disabled the contextmenu event, ensure
// our context menu cannot be prevented.
!composedTarget.nodePrincipal.isSystemPrincipal &&
!Services.prefs.getBoolPref("dom.event.contextmenu.enabled")
) {
defaultPrevented = false;
}

if (defaultPrevented) return;

const context = this.createContext(event, composedTarget);

console.log("ChildSend", composedTarget, context);

this.sendAsyncMessage("contextmenu", {
target: ContentDOMReference.get(composedTarget),

screenX: event.screenX,
screenY: event.screenY,

context
});
}

/**
* Fetches the context menu targets for an event
* @param {Event} event
* @returns {Set<Element>}
*/
#getEventContextMenuTargets(event) {
const eventBubblePath = /** @type {Element[]} */ (event.composedPath());

const contextMenuTargets = new Set();

switch (message.name) {
case "ContextMenu:ReloadFrame": {
const { forceReload } = message.data;
for (const node of eventBubblePath) {
// Checks if the node is actually an element
if (!node || !node.getAttribute) continue;

const target = ContentDOMReference.resolve(targetIdentifier);
// If we bubble through an element without a contextmenu,
// continue to the next element in the bubble path.
const contextMenuId = node.getAttribute("contextmenu");
if (!contextMenuId) continue;

/** @type {any} */ (target.ownerDocument.location).reload(
forceReload
// Checks whether it bubbles through a customizable area implementation
const implementsContext =
BrowserCustomizableShared.isCustomizableAreaImplementation(
node
);

/** @type {import("third_party/dothq/gecko-types/lib").XULPopupElement} */
let contextMenu = null;

if (contextMenuId && contextMenuId.length) {
// if contextMenu == _child, look for first <menupopup> child
if (contextMenuId == "_child") {
contextMenu = node.querySelector("menupopup");
} else {
const contextMenuEl =
node.ownerDocument.getElementById(contextMenuId);

if (contextMenuEl) {
if (contextMenuEl.tagName == "menupopup") {
contextMenu =
/** @type {import("third_party/dothq/gecko-types/lib").XULPopupElement} */ (
contextMenuEl
);
}
}
}
}

if (!contextMenu) continue;

contextMenuTargets.add(contextMenu);

// If we hit a non-contextual element, like a button, stop iterating
// as we cannot inherit any more items from further up in the bubble path.
if (!implementsContext) {
break;
}
}

return contextMenuTargets;
}

/**
* Creates a new context menu context object from an event
* @param {MouseEvent} event
* Fired when a context menu is launched
* @param {CustomEvent<{ context: Record<string, any>; screenX: number; screenY: number }>} event
*/
_createContext(event) {
return {};
#onContextMenuLaunch(event) {
const { screenX, screenY } = event.detail;

const target = /** @type {Node} */ (event.composedTarget);

const contextMenuTargets = this.#getEventContextMenuTargets(event);
if (!contextMenuTargets.size) return;

const contextMenuItems = Array.from(contextMenuTargets.values())
.map((t) => Array.from(t.childNodes))
.reduce((prev, curr) => (prev || []).concat(curr))
.map((i) => i.cloneNode(true));

const contextMenu =
/** @type {import("third_party/dothq/gecko-types/lib").XULPopupElement} */ (
target.ownerDocument.getElementById(
"constructed-context-menu"
) || target.ownerDocument.createXULElement("menupopup")
);

contextMenu.id = "constructed-context-menu";
contextMenu.replaceChildren(...contextMenuItems);

if (!contextMenu.parentElement) {
target.ownerDocument
.querySelector("popupset")
.appendChild(contextMenu);
}
contextMenu.openPopupAtScreen(screenX, screenY, true, event);

console.log("ChildReceive", target, contextMenuItems);
}

/**
* Receives incoming contextmenu events
* @param {MouseEvent} event
* Handles incoming events to the context menu child
* @param {Event} event
*/
async handleEvent(event) {
const context = this._createContext(event);

this.sendAsyncMessage("contextmenu", {
x: event.screenX,
y: event.screenY,

context
});
handleEvent(event) {
switch (event.type) {
case "contextmenu": {
this.#onContextMenuRequest(/** @type {MouseEvent} */ (event));
break;
}
case "ContextMenu::Launch": {
this.#onContextMenuLaunch(/** @type {CustomEvent} */ (event));
break;
}
}
}
}
50 changes: 32 additions & 18 deletions actors/DotContextMenuParent.sys.mjs
Original file line number Diff line number Diff line change
@@ -1,29 +1,43 @@
/* 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/. */
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */

const { ContentDOMReference } = ChromeUtils.importESModule(
"resource://gre/modules/ContentDOMReference.sys.mjs"
);

export class DotContextMenuParent extends JSWindowActorParent {
/** @param {import("third_party/dothq/gecko-types/lib").ReceiveMessageArgument<>} message */
/**
* Handles incoming messages to the context menu parent
* @param {import("third_party/dothq/gecko-types/lib").ReceiveMessageArgument} message
*/
receiveMessage(message) {
if (message.name !== "contextmenu") return;
// Attempt to resolve the target in the event,
// otherwise, check if we're inside a browser,
// otehrwise, use the top chrome window.
const target = /** @type {Element} */ (
ContentDOMReference.resolve(message.data.target) ||
this.browsingContext.embedderElement ||
this.browsingContext.topChromeWindow
);

const browser = this.manager.rootFrameLoader.ownerElement;
const win = browser.ownerGlobal;
const win = target.ownerGlobal;
const doc = target.ownerDocument;

// Make sure this browser belongs to us before we open the panel
if (win.gDot && win.gDot.tabs.getTabForWebContents(browser)) {
const { x, y, context } = message.data;
console.log("ParentReceive", target, message.data.context);

console.log(x, y, context);
}
}
const menuEvent = new /** @type {any} */ (win).CustomEvent(
"ContextMenu::Launch",
{
detail: message.data,
composed: true,
bubbles: true
}
);

console.log("ParentSend", target, menuEvent);

target.dispatchEvent(menuEvent);

hiding() {
try {
this.sendAsyncMessage("ContextMenu:Hiding", {});
} catch (e) {
// This will throw if the content goes away while the
// context menu is still open.
}
}
}
6 changes: 6 additions & 0 deletions third_party/dothq/gecko-types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,8 @@ declare global {
pressure: number,
inputSource: number
) => void;

clickEventPrevented: () => boolean;
}

interface XULElementWithCommandHandler {
Expand All @@ -190,6 +192,10 @@ declare global {
InspectorUtils: Gecko.InspectorUtils;
windowGlobalChild: Gecko.WindowGlobalChildInstance;
windowUtils: Gecko.WindowUtils;
scrollMaxX: number;
scrollMaxY: number;
scrollMinX: number;
scrollMinY: number;
}

interface Element extends Gecko.CustomElement {
Expand Down
2 changes: 2 additions & 0 deletions third_party/dothq/gecko-types/lib/BrowsingContext.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,6 @@ export interface BrowsingContext {
currentWindowGlobal: WindowGlobalParent;

group: BrowsingContextGroup;

topChromeWindow?: ChromeWindow;
}
58 changes: 34 additions & 24 deletions third_party/dothq/gecko-types/lib/WindowUtils.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,11 @@ export interface WindowUtils {
height: number
): DOMRect;

/**
* Transform a rectangle given in coordinates relative to this document
* into CSS coordinates relative to the screen.
*/
toScreenRectInCSSUnits(
/**
* Transform a rectangle given in coordinates relative to this document
* into CSS coordinates relative to the screen.
*/
toScreenRectInCSSUnits(
x: number,
y: number,
width: number,
Expand All @@ -48,24 +48,34 @@ export interface WindowUtils {
*/
getBoundsWithoutFlushing(element: Element): DOMRect;

AGENT_SHEET: 0;
USER_SHEET: 1;
AUTHOR_SHEET: 2;
/**
* Synchronously loads a style sheet from |sheetURI| and adds it to the list
* of additional style sheets of the document.
*
* These additional style sheets are very much like user/agent sheets loaded
* with loadAndRegisterSheet. The only difference is that they are applied only
* on the document owned by this window.
*
* Sheets added via this API take effect immediately on the document.
*/
loadSheet(sheetURI: nsIURI, type: number): void;

/**
* Same as the above method but allows passing the URI as a string.
*/
loadSheetUsingURIString(sheetURI: string, type: number): void;

AGENT_SHEET: 0;
USER_SHEET: 1;
AUTHOR_SHEET: 2;
/**
* Synchronously loads a style sheet from |sheetURI| and adds it to the list
* of additional style sheets of the document.
*
* These additional style sheets are very much like user/agent sheets loaded
* with loadAndRegisterSheet. The only difference is that they are applied only
* on the document owned by this window.
*
* Sheets added via this API take effect immediately on the document.
*/
loadSheet(sheetURI: nsIURI, type: number): void;

/**
* Same as the above method but allows passing the URI as a string.
*/
loadSheetUsingURIString(sheetURI: string, type: number): void;
/**
* Sets WidgetEvent::mFlags::mOnlyChromeDispatch to true to ensure that
* the event is propagated only to chrome.
* Event's .target property will be aTarget.
* Returns the same value as what EventTarget.dispatchEvent does.
*/
dispatchEventToChromeOnly(
target: EventTarget,
event: Event
): boolean;
}
5 changes: 5 additions & 0 deletions types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ declare global {
interface Event {
defaultCancelled: boolean;
defaultPreventedByChrome: boolean;
composedTarget: EventTarget;
}

interface Console {
Expand Down Expand Up @@ -169,4 +170,8 @@ declare global {
scrollLeftMin: number;
scrollLeftMax: number;
}

interface Node {
nodePrincipal: any /** @todo: nsIPrincipal */;
}
}

0 comments on commit 5b6579b

Please sign in to comment.