summaryrefslogtreecommitdiffstats
path: root/comm/mail/components/extensions/parent/ext-windows.js
diff options
context:
space:
mode:
Diffstat (limited to 'comm/mail/components/extensions/parent/ext-windows.js')
-rw-r--r--comm/mail/components/extensions/parent/ext-windows.js555
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);
+ },
+ },
+ };
+ }
+};