diff options
Diffstat (limited to '')
-rw-r--r-- | comm/mail/components/extensions/parent/ext-windows.js | 555 |
1 files changed, 555 insertions, 0 deletions
diff --git a/comm/mail/components/extensions/parent/ext-windows.js b/comm/mail/components/extensions/parent/ext-windows.js new file mode 100644 index 0000000000..6a3078d7d3 --- /dev/null +++ b/comm/mail/components/extensions/parent/ext-windows.js @@ -0,0 +1,555 @@ +/* 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/. */ + +// The ext-* files are imported into the same scopes. +/* import-globals-from ext-mail.js */ + +function sanitizePositionParams(params, window = null, positionOffset = 0) { + if (params.left === null && params.top === null) { + return; + } + + if (params.left === null) { + const baseLeft = window ? window.screenX : 0; + params.left = baseLeft + positionOffset; + } + if (params.top === null) { + const baseTop = window ? window.screenY : 0; + params.top = baseTop + positionOffset; + } + + // boundary check: don't put window out of visible area + const baseWidth = window ? window.outerWidth : 0; + const baseHeight = window ? window.outerHeight : 0; + // Secure minimum size of an window should be same to the one + // defined at nsGlobalWindowOuter::CheckSecurityWidthAndHeight. + const minWidth = 100; + const minHeight = 100; + const width = Math.max( + minWidth, + params.width !== null ? params.width : baseWidth + ); + const height = Math.max( + minHeight, + params.height !== null ? params.height : baseHeight + ); + const screenManager = Cc["@mozilla.org/gfx/screenmanager;1"].getService( + Ci.nsIScreenManager + ); + const screen = screenManager.screenForRect( + params.left, + params.top, + width, + height + ); + const availDeviceLeft = {}; + const availDeviceTop = {}; + const availDeviceWidth = {}; + const availDeviceHeight = {}; + screen.GetAvailRect( + availDeviceLeft, + availDeviceTop, + availDeviceWidth, + availDeviceHeight + ); + const factor = screen.defaultCSSScaleFactor; + const availLeft = Math.floor(availDeviceLeft.value / factor); + const availTop = Math.floor(availDeviceTop.value / factor); + const availWidth = Math.floor(availDeviceWidth.value / factor); + const availHeight = Math.floor(availDeviceHeight.value / factor); + params.left = Math.min( + availLeft + availWidth - width, + Math.max(availLeft, params.left) + ); + params.top = Math.min( + availTop + availHeight - height, + Math.max(availTop, params.top) + ); +} + +/** + * Update the geometry of the mail window. + * + * @param {object} options + * An object containing new values for the window's geometry. + * @param {integer} [options.left] + * The new pixel distance of the left side of the mail window from + * the left of the screen. + * @param {integer} [options.top] + * The new pixel distance of the top side of the mail window from + * the top of the screen. + * @param {integer} [options.width] + * The new pixel width of the window. + * @param {integer} [options.height] + * The new pixel height of the window. + */ +function updateGeometry(window, options) { + if (options.left !== null || options.top !== null) { + let left = options.left === null ? window.screenX : options.left; + let top = options.top === null ? window.screenY : options.top; + window.moveTo(left, top); + } + + if (options.width !== null || options.height !== null) { + let width = options.width === null ? window.outerWidth : options.width; + let height = options.height === null ? window.outerHeight : options.height; + window.resizeTo(width, height); + } +} + +this.windows = class extends ExtensionAPIPersistent { + onShutdown(isAppShutdown) { + if (isAppShutdown) { + return; + } + for (let window of Services.wm.getEnumerator("mail:extensionPopup")) { + let uri = window.browser.browsingContext.currentURI; + if (uri.scheme == "moz-extension" && uri.host == this.extension.uuid) { + window.close(); + } + } + } + + windowEventRegistrar({ windowEvent, listener }) { + let { extension } = this; + return ({ context, fire }) => { + let listener2 = async (window, ...args) => { + if (!extension.canAccessWindow(window)) { + return; + } + if (fire.wakeup) { + await fire.wakeup(); + } + listener({ context, fire, window }, ...args); + }; + windowTracker.addListener(windowEvent, listener2); + return { + unregister() { + windowTracker.removeListener(windowEvent, listener2); + }, + convert(newFire, extContext) { + fire = newFire; + context = extContext; + }, + }; + }; + } + + PERSISTENT_EVENTS = { + // For primed persistent events (deactivated background), the context is only + // available after fire.wakeup() has fulfilled (ensuring the convert() function + // has been called) (handled by windowEventRegistrar). + + onCreated: this.windowEventRegistrar({ + windowEvent: "domwindowopened", + listener: async ({ context, fire, window }) => { + // Return the window only after it has been fully initialized. + if (window.webExtensionWindowCreatePending) { + await new Promise(resolve => { + window.addEventListener("webExtensionWindowCreateDone", resolve, { + once: true, + }); + }); + } + fire.async(this.extension.windowManager.convert(window)); + }, + }), + + onRemoved: this.windowEventRegistrar({ + windowEvent: "domwindowclosed", + listener: ({ context, fire, window }) => { + fire.async(windowTracker.getId(window)); + }, + }), + + onFocusChanged({ context, fire }) { + let { extension } = this; + // Keep track of the last windowId used to fire an onFocusChanged event + let lastOnFocusChangedWindowId; + let scheduledEvents = []; + + let listener = async event => { + // Wait a tick to avoid firing a superfluous WINDOW_ID_NONE + // event when switching focus between two Thunderbird windows. + // Note: This is not working for Linux, where we still get the -1 + await Promise.resolve(); + + let windowId = WindowBase.WINDOW_ID_NONE; + let window = Services.focus.activeWindow; + if (window) { + if (!extension.canAccessWindow(window)) { + return; + } + windowId = windowTracker.getId(window); + } + + // Using a FIFO to keep order of events, in case the last one + // gets through without being placed on the async callback stack. + scheduledEvents.push(windowId); + if (fire.wakeup) { + await fire.wakeup(); + } + let scheduledWindowId = scheduledEvents.shift(); + + if (scheduledWindowId !== lastOnFocusChangedWindowId) { + lastOnFocusChangedWindowId = scheduledWindowId; + fire.async(scheduledWindowId); + } + }; + windowTracker.addListener("focus", listener); + windowTracker.addListener("blur", listener); + return { + unregister() { + windowTracker.removeListener("focus", listener); + windowTracker.removeListener("blur", listener); + }, + convert(newFire, extContext) { + fire = newFire; + context = extContext; + }, + }; + }, + }; + + getAPI(context) { + const { extension } = context; + const { windowManager } = extension; + + return { + windows: { + onCreated: new EventManager({ + context, + module: "windows", + event: "onCreated", + extensionApi: this, + }).api(), + + onRemoved: new EventManager({ + context, + module: "windows", + event: "onRemoved", + extensionApi: this, + }).api(), + + onFocusChanged: new EventManager({ + context, + module: "windows", + event: "onFocusChanged", + extensionApi: this, + }).api(), + + get(windowId, getInfo) { + let window = windowTracker.getWindow(windowId, context); + if (!window) { + return Promise.reject({ + message: `Invalid window ID: ${windowId}`, + }); + } + return Promise.resolve(windowManager.convert(window, getInfo)); + }, + + async getCurrent(getInfo) { + let window = context.currentWindow || windowTracker.topWindow; + if (window.document.readyState != "complete") { + await new Promise(resolve => + window.addEventListener("load", resolve, { once: true }) + ); + } + return windowManager.convert(window, getInfo); + }, + + async getLastFocused(getInfo) { + let window = windowTracker.topWindow; + if (window.document.readyState != "complete") { + await new Promise(resolve => + window.addEventListener("load", resolve, { once: true }) + ); + } + return windowManager.convert(window, getInfo); + }, + + getAll(getInfo) { + let doNotCheckTypes = !getInfo || !getInfo.windowTypes; + + let windows = Array.from(windowManager.getAll(), win => + win.convert(getInfo) + ).filter( + win => doNotCheckTypes || getInfo.windowTypes.includes(win.type) + ); + return Promise.resolve(windows); + }, + + async create(createData) { + if (createData.incognito) { + throw new ExtensionError("`incognito` is not supported"); + } + + let needResize = + createData.left !== null || + createData.top !== null || + createData.width !== null || + createData.height !== null; + if (needResize) { + if (createData.state !== null && createData.state != "normal") { + throw new ExtensionError( + `"state": "${createData.state}" may not be combined with "left", "top", "width", or "height"` + ); + } + createData.state = "normal"; + } + + // 10px offset is same to Chromium + sanitizePositionParams(createData, windowTracker.topNormalWindow, 10); + + let userContextId = + Services.scriptSecurityManager.DEFAULT_USER_CONTEXT_ID; + if (createData.cookieStoreId) { + userContextId = getUserContextIdForCookieStoreId( + extension, + createData.cookieStoreId + ); + } + let createWindowArgs = createData => { + let allowScriptsToClose = !!createData.allowScriptsToClose; + let url = createData.url || "about:blank"; + let urls = Array.isArray(url) ? url : [url]; + + let args = Cc["@mozilla.org/array;1"].createInstance( + Ci.nsIMutableArray + ); + let actionData = { + action: "open", + allowScriptsToClose, + tabs: urls.map(url => ({ + tabType: "contentTab", + tabParams: { url, userContextId }, + })), + }; + actionData.wrappedJSObject = actionData; + args.appendElement(null); + args.appendElement(actionData); + return args; + }; + + let window; + let wantNormalWindow = + createData.type === null || createData.type == "normal"; + let features = ["chrome"]; + if (wantNormalWindow) { + features.push("dialog=no", "all", "status", "toolbar"); + } else { + // All other types create "popup"-type windows by default. + // Use dialog=no to get minimize and maximize buttons (as chrome + // does) and to allow the API to actually maximize the popup in + // Linux. + features.push( + "dialog=no", + "resizable", + "minimizable", + "titlebar", + "close" + ); + if (createData.left === null && createData.top === null) { + features.push("centerscreen"); + } + } + + let windowURL = wantNormalWindow + ? "chrome://messenger/content/messenger.xhtml" + : "chrome://messenger/content/extensionPopup.xhtml"; + if (createData.tabId) { + if (createData.url) { + return Promise.reject({ + message: "`tabId` may not be used in conjunction with `url`", + }); + } + + if (createData.allowScriptsToClose) { + return Promise.reject({ + message: + "`tabId` may not be used in conjunction with `allowScriptsToClose`", + }); + } + + if (createData.cookieStoreId) { + return Promise.reject({ + message: + "`tabId` may not be used in conjunction with `cookieStoreId`", + }); + } + + let nativeTabInfo = tabTracker.getTab(createData.tabId); + let tabmail = + getTabBrowser(nativeTabInfo).ownerDocument.getElementById( + "tabmail" + ); + let targetType = wantNormalWindow ? null : "popup"; + window = tabmail.replaceTabWithWindow(nativeTabInfo, targetType)[0]; + } else { + window = Services.ww.openWindow( + null, + windowURL, + "_blank", + features.join(","), + wantNormalWindow ? null : createWindowArgs(createData) + ); + } + + window.webExtensionWindowCreatePending = true; + + updateGeometry(window, createData); + + // TODO: focused, type + + // Wait till the newly created window is focused. On Linux the initial + // "normal" state has been set once the window has been fully focused. + // Setting a different state before the window is fully focused may cause + // the initial state to be erroneously applied after the custom state has + // been set. + let focusPromise = new Promise(resolve => { + if (Services.focus.activeWindow == window) { + resolve(); + } else { + window.addEventListener("focus", resolve, { once: true }); + } + }); + + let loadPromise = new Promise(resolve => { + window.addEventListener("load", resolve, { once: true }); + }); + + let titlePromise = new Promise(resolve => { + window.addEventListener("pagetitlechanged", resolve, { + once: true, + }); + }); + + await Promise.all([focusPromise, loadPromise, titlePromise]); + + let win = windowManager.getWrapper(window); + + if ( + [ + "minimized", + "fullscreen", + "docked", + "normal", + "maximized", + ].includes(createData.state) + ) { + await win.setState(createData.state); + } + + if (createData.titlePreface !== null) { + win.setTitlePreface(createData.titlePreface); + } + + // Update the title independently of a createData.titlePreface, to get + // the title of the loaded document into the window title. + if (win instanceof TabmailWindow) { + win.window.document.getElementById("tabmail").setDocumentTitle(); + } else if (win.window.gBrowser?.updateTitlebar) { + await win.window.gBrowser.updateTitlebar(); + } + + delete window.webExtensionWindowCreatePending; + window.dispatchEvent( + new window.CustomEvent("webExtensionWindowCreateDone") + ); + return win.convert({ populate: true }); + }, + + async update(windowId, updateInfo) { + let needResize = + updateInfo.left !== null || + updateInfo.top !== null || + updateInfo.width !== null || + updateInfo.height !== null; + if ( + updateInfo.state !== null && + updateInfo.state != "normal" && + needResize + ) { + throw new ExtensionError( + `"state": "${updateInfo.state}" may not be combined with "left", "top", "width", or "height"` + ); + } + + let win = windowManager.get(windowId, context); + if (!win) { + throw new ExtensionError(`Invalid window ID: ${windowId}`); + } + + // Update the window only after it has been fully initialized. + if (win.window.webExtensionWindowCreatePending) { + await new Promise(resolve => { + win.window.addEventListener( + "webExtensionWindowCreateDone", + resolve, + { once: true } + ); + }); + } + + if (updateInfo.focused) { + win.window.focus(); + } + + if (updateInfo.state !== null) { + await win.setState(updateInfo.state); + } + + if (updateInfo.drawAttention) { + // Bug 1257497 - Firefox can't cancel attention actions. + win.window.getAttention(); + } + + updateGeometry(win.window, updateInfo); + + if (updateInfo.titlePreface !== null) { + win.setTitlePreface(updateInfo.titlePreface); + if (win instanceof TabmailWindow) { + win.window.document.getElementById("tabmail").setDocumentTitle(); + } else if (win.window.gBrowser?.updateTitlebar) { + await win.window.gBrowser.updateTitlebar(); + } + } + + // TODO: All the other properties, focused=false... + + return win.convert(); + }, + + remove(windowId) { + let window = windowTracker.getWindow(windowId, context); + window.close(); + + return new Promise(resolve => { + let listener = () => { + windowTracker.removeListener("domwindowclosed", listener); + resolve(); + }; + windowTracker.addListener("domwindowclosed", listener); + }); + }, + openDefaultBrowser(url) { + let uri = null; + try { + uri = Services.io.newURI(url); + } catch (e) { + throw new ExtensionError(`Url "${url}" seems to be malformed.`); + } + if (!uri.schemeIs("http") && !uri.schemeIs("https")) { + throw new ExtensionError( + `Url scheme "${uri.scheme}" is not supported.` + ); + } + Cc["@mozilla.org/uriloader/external-protocol-service;1"] + .getService(Ci.nsIExternalProtocolService) + .loadURI(uri); + }, + }, + }; + } +}; |