Skip to content

Commit 5b6579b

Browse files
committed
🚧 WIP: Begin rework of context menu actors
1 parent 1763691 commit 5b6579b

File tree

6 files changed

+237
-66
lines changed

6 files changed

+237
-66
lines changed

‎actors/DotContextMenuChild.sys.mjs

Lines changed: 158 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,47 +6,181 @@ const { ContentDOMReference } = ChromeUtils.importESModule(
66
"resource://gre/modules/ContentDOMReference.sys.mjs"
77
);
88

9+
const { DOMUtils } = ChromeUtils.importESModule(
10+
"resource://gre/modules/DOMUtils.sys.mjs"
11+
);
12+
13+
const { BrowserCustomizableShared } = ChromeUtils.importESModule(
14+
"resource://gre/modules/BrowserCustomizableShared.sys.mjs"
15+
);
16+
17+
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
18+
919
export class DotContextMenuChild extends JSWindowActorChild {
1020
/**
11-
* @param {import("third_party/dothq/gecko-types/lib").ReceiveMessageArgument} message
21+
* Creates a new context object
22+
* @param {Event} event
23+
* @param {Node} target
1224
*/
13-
receiveMessage(message) {
14-
const { targetIdentifier } = message.data;
25+
createContext(event, target) {
26+
return {};
27+
}
28+
29+
/**
30+
* Fired when a context menu is requested via the contextmenu DOM event
31+
* @param {MouseEvent} event
32+
*/
33+
#onContextMenuRequest(event) {
34+
let { defaultPrevented } = event;
35+
36+
const composedTarget = /** @type {Element} */ (event.composedTarget);
37+
38+
// Ignore contextmenu events on a chrome browser, as we'll
39+
// handle the contextmenu event from inside the child content.
40+
if (
41+
composedTarget.namespaceURI == XUL_NS &&
42+
composedTarget.tagName == "browser"
43+
)
44+
return;
45+
46+
if (
47+
// If the event originated from a non-chrome document
48+
// and we have disabled the contextmenu event, ensure
49+
// our context menu cannot be prevented.
50+
!composedTarget.nodePrincipal.isSystemPrincipal &&
51+
!Services.prefs.getBoolPref("dom.event.contextmenu.enabled")
52+
) {
53+
defaultPrevented = false;
54+
}
55+
56+
if (defaultPrevented) return;
57+
58+
const context = this.createContext(event, composedTarget);
59+
60+
console.log("ChildSend", composedTarget, context);
61+
62+
this.sendAsyncMessage("contextmenu", {
63+
target: ContentDOMReference.get(composedTarget),
64+
65+
screenX: event.screenX,
66+
screenY: event.screenY,
67+
68+
context
69+
});
70+
}
71+
72+
/**
73+
* Fetches the context menu targets for an event
74+
* @param {Event} event
75+
* @returns {Set<Element>}
76+
*/
77+
#getEventContextMenuTargets(event) {
78+
const eventBubblePath = /** @type {Element[]} */ (event.composedPath());
79+
80+
const contextMenuTargets = new Set();
1581

16-
switch (message.name) {
17-
case "ContextMenu:ReloadFrame": {
18-
const { forceReload } = message.data;
82+
for (const node of eventBubblePath) {
83+
// Checks if the node is actually an element
84+
if (!node || !node.getAttribute) continue;
1985

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

22-
/** @type {any} */ (target.ownerDocument.location).reload(
23-
forceReload
91+
// Checks whether it bubbles through a customizable area implementation
92+
const implementsContext =
93+
BrowserCustomizableShared.isCustomizableAreaImplementation(
94+
node
2495
);
96+
97+
/** @type {import("third_party/dothq/gecko-types/lib").XULPopupElement} */
98+
let contextMenu = null;
99+
100+
if (contextMenuId && contextMenuId.length) {
101+
// if contextMenu == _child, look for first <menupopup> child
102+
if (contextMenuId == "_child") {
103+
contextMenu = node.querySelector("menupopup");
104+
} else {
105+
const contextMenuEl =
106+
node.ownerDocument.getElementById(contextMenuId);
107+
108+
if (contextMenuEl) {
109+
if (contextMenuEl.tagName == "menupopup") {
110+
contextMenu =
111+
/** @type {import("third_party/dothq/gecko-types/lib").XULPopupElement} */ (
112+
contextMenuEl
113+
);
114+
}
115+
}
116+
}
117+
}
118+
119+
if (!contextMenu) continue;
120+
121+
contextMenuTargets.add(contextMenu);
122+
123+
// If we hit a non-contextual element, like a button, stop iterating
124+
// as we cannot inherit any more items from further up in the bubble path.
125+
if (!implementsContext) {
25126
break;
26127
}
27128
}
129+
130+
return contextMenuTargets;
28131
}
29132

30133
/**
31-
* Creates a new context menu context object from an event
32-
* @param {MouseEvent} event
134+
* Fired when a context menu is launched
135+
* @param {CustomEvent<{ context: Record<string, any>; screenX: number; screenY: number }>} event
33136
*/
34-
_createContext(event) {
35-
return {};
137+
#onContextMenuLaunch(event) {
138+
const { screenX, screenY } = event.detail;
139+
140+
const target = /** @type {Node} */ (event.composedTarget);
141+
142+
const contextMenuTargets = this.#getEventContextMenuTargets(event);
143+
if (!contextMenuTargets.size) return;
144+
145+
const contextMenuItems = Array.from(contextMenuTargets.values())
146+
.map((t) => Array.from(t.childNodes))
147+
.reduce((prev, curr) => (prev || []).concat(curr))
148+
.map((i) => i.cloneNode(true));
149+
150+
const contextMenu =
151+
/** @type {import("third_party/dothq/gecko-types/lib").XULPopupElement} */ (
152+
target.ownerDocument.getElementById(
153+
"constructed-context-menu"
154+
) || target.ownerDocument.createXULElement("menupopup")
155+
);
156+
157+
contextMenu.id = "constructed-context-menu";
158+
contextMenu.replaceChildren(...contextMenuItems);
159+
160+
if (!contextMenu.parentElement) {
161+
target.ownerDocument
162+
.querySelector("popupset")
163+
.appendChild(contextMenu);
164+
}
165+
contextMenu.openPopupAtScreen(screenX, screenY, true, event);
166+
167+
console.log("ChildReceive", target, contextMenuItems);
36168
}
37169

38170
/**
39-
* Receives incoming contextmenu events
40-
* @param {MouseEvent} event
171+
* Handles incoming events to the context menu child
172+
* @param {Event} event
41173
*/
42-
async handleEvent(event) {
43-
const context = this._createContext(event);
44-
45-
this.sendAsyncMessage("contextmenu", {
46-
x: event.screenX,
47-
y: event.screenY,
48-
49-
context
50-
});
174+
handleEvent(event) {
175+
switch (event.type) {
176+
case "contextmenu": {
177+
this.#onContextMenuRequest(/** @type {MouseEvent} */ (event));
178+
break;
179+
}
180+
case "ContextMenu::Launch": {
181+
this.#onContextMenuLaunch(/** @type {CustomEvent} */ (event));
182+
break;
183+
}
184+
}
51185
}
52186
}
Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,43 @@
11
/* This Source Code Form is subject to the terms of the Mozilla Public
22
* License, v. 2.0. If a copy of the MPL was not distributed with this
3-
* file, you can obtain one at http://mozilla.org/MPL/2.0/. */
3+
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4+
5+
const { ContentDOMReference } = ChromeUtils.importESModule(
6+
"resource://gre/modules/ContentDOMReference.sys.mjs"
7+
);
48

59
export class DotContextMenuParent extends JSWindowActorParent {
6-
/** @param {import("third_party/dothq/gecko-types/lib").ReceiveMessageArgument<>} message */
10+
/**
11+
* Handles incoming messages to the context menu parent
12+
* @param {import("third_party/dothq/gecko-types/lib").ReceiveMessageArgument} message
13+
*/
714
receiveMessage(message) {
8-
if (message.name !== "contextmenu") return;
15+
// Attempt to resolve the target in the event,
16+
// otherwise, check if we're inside a browser,
17+
// otehrwise, use the top chrome window.
18+
const target = /** @type {Element} */ (
19+
ContentDOMReference.resolve(message.data.target) ||
20+
this.browsingContext.embedderElement ||
21+
this.browsingContext.topChromeWindow
22+
);
923

10-
const browser = this.manager.rootFrameLoader.ownerElement;
11-
const win = browser.ownerGlobal;
24+
const win = target.ownerGlobal;
25+
const doc = target.ownerDocument;
1226

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

17-
console.log(x, y, context);
18-
}
19-
}
29+
const menuEvent = new /** @type {any} */ (win).CustomEvent(
30+
"ContextMenu::Launch",
31+
{
32+
detail: message.data,
33+
composed: true,
34+
bubbles: true
35+
}
36+
);
37+
38+
console.log("ParentSend", target, menuEvent);
39+
40+
target.dispatchEvent(menuEvent);
2041

21-
hiding() {
22-
try {
23-
this.sendAsyncMessage("ContextMenu:Hiding", {});
24-
} catch (e) {
25-
// This will throw if the content goes away while the
26-
// context menu is still open.
27-
}
2842
}
2943
}

‎third_party/dothq/gecko-types/index.d.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,8 @@ declare global {
168168
pressure: number,
169169
inputSource: number
170170
) => void;
171+
172+
clickEventPrevented: () => boolean;
171173
}
172174

173175
interface XULElementWithCommandHandler {
@@ -190,6 +192,10 @@ declare global {
190192
InspectorUtils: Gecko.InspectorUtils;
191193
windowGlobalChild: Gecko.WindowGlobalChildInstance;
192194
windowUtils: Gecko.WindowUtils;
195+
scrollMaxX: number;
196+
scrollMaxY: number;
197+
scrollMinX: number;
198+
scrollMinY: number;
193199
}
194200

195201
interface Element extends Gecko.CustomElement {

‎third_party/dothq/gecko-types/lib/BrowsingContext.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,6 @@ export interface BrowsingContext {
5050
currentWindowGlobal: WindowGlobalParent;
5151

5252
group: BrowsingContextGroup;
53+
54+
topChromeWindow?: ChromeWindow;
5355
}

‎third_party/dothq/gecko-types/lib/WindowUtils.d.ts

Lines changed: 34 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,11 @@ export interface WindowUtils {
2020
height: number
2121
): DOMRect;
2222

23-
/**
24-
* Transform a rectangle given in coordinates relative to this document
25-
* into CSS coordinates relative to the screen.
26-
*/
27-
toScreenRectInCSSUnits(
23+
/**
24+
* Transform a rectangle given in coordinates relative to this document
25+
* into CSS coordinates relative to the screen.
26+
*/
27+
toScreenRectInCSSUnits(
2828
x: number,
2929
y: number,
3030
width: number,
@@ -48,24 +48,34 @@ export interface WindowUtils {
4848
*/
4949
getBoundsWithoutFlushing(element: Element): DOMRect;
5050

51+
AGENT_SHEET: 0;
52+
USER_SHEET: 1;
53+
AUTHOR_SHEET: 2;
54+
/**
55+
* Synchronously loads a style sheet from |sheetURI| and adds it to the list
56+
* of additional style sheets of the document.
57+
*
58+
* These additional style sheets are very much like user/agent sheets loaded
59+
* with loadAndRegisterSheet. The only difference is that they are applied only
60+
* on the document owned by this window.
61+
*
62+
* Sheets added via this API take effect immediately on the document.
63+
*/
64+
loadSheet(sheetURI: nsIURI, type: number): void;
65+
66+
/**
67+
* Same as the above method but allows passing the URI as a string.
68+
*/
69+
loadSheetUsingURIString(sheetURI: string, type: number): void;
5170

52-
AGENT_SHEET: 0;
53-
USER_SHEET: 1;
54-
AUTHOR_SHEET: 2;
55-
/**
56-
* Synchronously loads a style sheet from |sheetURI| and adds it to the list
57-
* of additional style sheets of the document.
58-
*
59-
* These additional style sheets are very much like user/agent sheets loaded
60-
* with loadAndRegisterSheet. The only difference is that they are applied only
61-
* on the document owned by this window.
62-
*
63-
* Sheets added via this API take effect immediately on the document.
64-
*/
65-
loadSheet(sheetURI: nsIURI, type: number): void;
66-
67-
/**
68-
* Same as the above method but allows passing the URI as a string.
69-
*/
70-
loadSheetUsingURIString(sheetURI: string, type: number): void;
71+
/**
72+
* Sets WidgetEvent::mFlags::mOnlyChromeDispatch to true to ensure that
73+
* the event is propagated only to chrome.
74+
* Event's .target property will be aTarget.
75+
* Returns the same value as what EventTarget.dispatchEvent does.
76+
*/
77+
dispatchEventToChromeOnly(
78+
target: EventTarget,
79+
event: Event
80+
): boolean;
7181
}

‎types.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ declare global {
110110
interface Event {
111111
defaultCancelled: boolean;
112112
defaultPreventedByChrome: boolean;
113+
composedTarget: EventTarget;
113114
}
114115

115116
interface Console {
@@ -169,4 +170,8 @@ declare global {
169170
scrollLeftMin: number;
170171
scrollLeftMax: number;
171172
}
173+
174+
interface Node {
175+
nodePrincipal: any /** @todo: nsIPrincipal */;
176+
}
172177
}

0 commit comments

Comments
 (0)