michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: module.metadata = { michael@0: "stability": "unstable" michael@0: }; michael@0: michael@0: const { Cc, Ci } = require("chrome"); michael@0: const { setTimeout } = require("../timers"); michael@0: const { platform } = require("../system"); michael@0: const { getMostRecentBrowserWindow, getOwnerBrowserWindow, michael@0: getHiddenWindow, getScreenPixelsPerCSSPixel } = require("../window/utils"); michael@0: michael@0: const { create: createFrame, swapFrameLoaders } = require("../frame/utils"); michael@0: const { window: addonWindow } = require("../addon/window"); michael@0: const { isNil } = require("../lang/type"); michael@0: const events = require("../system/events"); michael@0: michael@0: michael@0: const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; michael@0: michael@0: function calculateRegion({ position, width, height, defaultWidth, defaultHeight }, rect) { michael@0: position = position || {}; michael@0: michael@0: let x, y; michael@0: michael@0: let hasTop = !isNil(position.top); michael@0: let hasRight = !isNil(position.right); michael@0: let hasBottom = !isNil(position.bottom); michael@0: let hasLeft = !isNil(position.left); michael@0: let hasWidth = !isNil(width); michael@0: let hasHeight = !isNil(height); michael@0: michael@0: // if width is not specified by constructor or show's options, then get michael@0: // the default width michael@0: if (!hasWidth) michael@0: width = defaultWidth; michael@0: michael@0: // if height is not specified by constructor or show's options, then get michael@0: // the default height michael@0: if (!hasHeight) michael@0: height = defaultHeight; michael@0: michael@0: // default position is centered michael@0: x = (rect.right - width) / 2; michael@0: y = (rect.top + rect.bottom - height) / 2; michael@0: michael@0: if (hasTop) { michael@0: y = rect.top + position.top; michael@0: michael@0: if (hasBottom && !hasHeight) michael@0: height = rect.bottom - position.bottom - y; michael@0: } michael@0: else if (hasBottom) { michael@0: y = rect.bottom - position.bottom - height; michael@0: } michael@0: michael@0: if (hasLeft) { michael@0: x = position.left; michael@0: michael@0: if (hasRight && !hasWidth) michael@0: width = rect.right - position.right - x; michael@0: } michael@0: else if (hasRight) { michael@0: x = rect.right - width - position.right; michael@0: } michael@0: michael@0: return {x: x, y: y, width: width, height: height}; michael@0: } michael@0: michael@0: function open(panel, options, anchor) { michael@0: // Wait for the XBL binding to be constructed michael@0: if (!panel.openPopup) setTimeout(open, 50, panel, options, anchor); michael@0: else display(panel, options, anchor); michael@0: } michael@0: exports.open = open; michael@0: michael@0: function isOpen(panel) { michael@0: return panel.state === "open" michael@0: } michael@0: exports.isOpen = isOpen; michael@0: michael@0: function isOpening(panel) { michael@0: return panel.state === "showing" michael@0: } michael@0: exports.isOpening = isOpening michael@0: michael@0: function close(panel) { michael@0: // Sometimes "TypeError: panel.hidePopup is not a function" is thrown michael@0: // when quitting the host application while a panel is visible. To suppress michael@0: // these errors, check for "hidePopup" in panel before calling it. michael@0: // It's not clear if there's an issue or it's expected behavior. michael@0: michael@0: return panel.hidePopup && panel.hidePopup(); michael@0: } michael@0: exports.close = close michael@0: michael@0: michael@0: function resize(panel, width, height) { michael@0: // Resize the iframe instead of using panel.sizeTo michael@0: // because sizeTo doesn't work with arrow panels michael@0: panel.firstChild.style.width = width + "px"; michael@0: panel.firstChild.style.height = height + "px"; michael@0: } michael@0: exports.resize = resize michael@0: michael@0: function display(panel, options, anchor) { michael@0: let document = panel.ownerDocument; michael@0: michael@0: let x, y; michael@0: let { width, height, defaultWidth, defaultHeight } = options; michael@0: michael@0: let popupPosition = null; michael@0: michael@0: // Panel XBL has some SDK incompatible styling decisions. We shim panel michael@0: // instances until proper fix for Bug 859504 is shipped. michael@0: shimDefaultStyle(panel); michael@0: michael@0: if (!anchor) { michael@0: // The XUL Panel doesn't have an arrow, so the margin needs to be reset michael@0: // in order to, be positioned properly michael@0: panel.style.margin = "0"; michael@0: michael@0: let viewportRect = document.defaultView.gBrowser.getBoundingClientRect(); michael@0: michael@0: ({x, y, width, height}) = calculateRegion(options, viewportRect); michael@0: } michael@0: else { michael@0: // The XUL Panel has an arrow, so the margin needs to be reset michael@0: // to the default value. michael@0: panel.style.margin = ""; michael@0: let { CustomizableUI, window } = anchor.ownerDocument.defaultView; michael@0: michael@0: // In Australis, widgets may be positioned in an overflow panel or the michael@0: // menu panel. michael@0: // In such cases clicking this widget will hide the overflow/menu panel, michael@0: // and the widget's panel will show instead. michael@0: // If `CustomizableUI` is not available, it means the anchor is not in a michael@0: // chrome browser window, and therefore there is no need for this check. michael@0: if (CustomizableUI) { michael@0: let node = anchor; michael@0: ({anchor}) = CustomizableUI.getWidget(anchor.id).forWindow(window); michael@0: michael@0: // if `node` is not the `anchor` itself, it means the widget is michael@0: // positioned in a panel, therefore we have to hide it before show michael@0: // the widget's panel in the same anchor michael@0: if (node !== anchor) michael@0: CustomizableUI.hidePanelForNode(anchor); michael@0: } michael@0: michael@0: width = width || defaultWidth; michael@0: height = height || defaultHeight; michael@0: michael@0: // Open the popup by the anchor. michael@0: let rect = anchor.getBoundingClientRect(); michael@0: michael@0: let zoom = getScreenPixelsPerCSSPixel(window); michael@0: let screenX = rect.left + window.mozInnerScreenX * zoom; michael@0: let screenY = rect.top + window.mozInnerScreenY * zoom; michael@0: michael@0: // Set up the vertical position of the popup relative to the anchor michael@0: // (always display the arrow on anchor center) michael@0: let horizontal, vertical; michael@0: if (screenY > window.screen.availHeight / 2 + height) michael@0: vertical = "top"; michael@0: else michael@0: vertical = "bottom"; michael@0: michael@0: if (screenY > window.screen.availWidth / 2 + width) michael@0: horizontal = "left"; michael@0: else michael@0: horizontal = "right"; michael@0: michael@0: let verticalInverse = vertical == "top" ? "bottom" : "top"; michael@0: popupPosition = vertical + "center " + verticalInverse + horizontal; michael@0: michael@0: // Allow panel to flip itself if the panel can't be displayed at the michael@0: // specified position (useful if we compute a bad position or if the michael@0: // user moves the window and panel remains visible) michael@0: panel.setAttribute("flip", "both"); michael@0: } michael@0: michael@0: // Resize the iframe instead of using panel.sizeTo michael@0: // because sizeTo doesn't work with arrow panels michael@0: panel.firstChild.style.width = width + "px"; michael@0: panel.firstChild.style.height = height + "px"; michael@0: michael@0: panel.openPopup(anchor, popupPosition, x, y); michael@0: } michael@0: exports.display = display; michael@0: michael@0: // This utility function is just a workaround until Bug 859504 has shipped. michael@0: function shimDefaultStyle(panel) { michael@0: let document = panel.ownerDocument; michael@0: // Please note that `panel` needs to be part of document in order to reach michael@0: // it's anonymous nodes. One of the anonymous node has a big padding which michael@0: // doesn't work well since panel frame needs to fill all of the panel. michael@0: // XBL binding is a not the best option as it's applied asynchronously, and michael@0: // makes injected frames behave in strange way. Also this feels a lot michael@0: // cheaper to do. michael@0: ["panel-inner-arrowcontent", "panel-arrowcontent"].forEach(function(value) { michael@0: let node = document.getAnonymousElementByAttribute(panel, "class", value); michael@0: if (node) node.style.padding = 0; michael@0: }); michael@0: } michael@0: michael@0: function show(panel, options, anchor) { michael@0: // Prevent the panel from getting focus when showing up michael@0: // if focus is set to false michael@0: panel.setAttribute("noautofocus", !options.focus); michael@0: michael@0: let window = anchor && getOwnerBrowserWindow(anchor); michael@0: let { document } = window ? window : getMostRecentBrowserWindow(); michael@0: attach(panel, document); michael@0: michael@0: open(panel, options, anchor); michael@0: } michael@0: exports.show = show michael@0: michael@0: function setupPanelFrame(frame) { michael@0: frame.setAttribute("flex", 1); michael@0: frame.setAttribute("transparent", "transparent"); michael@0: frame.setAttribute("autocompleteenabled", true); michael@0: if (platform === "darwin") { michael@0: frame.style.borderRadius = "6px"; michael@0: frame.style.padding = "1px"; michael@0: } michael@0: } michael@0: michael@0: function make(document) { michael@0: document = document || getMostRecentBrowserWindow().document; michael@0: let panel = document.createElementNS(XUL_NS, "panel"); michael@0: panel.setAttribute("type", "arrow"); michael@0: michael@0: // Note that panel is a parent of `viewFrame` who's `docShell` will be michael@0: // configured at creation time. If `panel` and there for `viewFrame` won't michael@0: // have an owner document attempt to access `docShell` will throw. There michael@0: // for we attach panel to a document. michael@0: attach(panel, document); michael@0: michael@0: let frameOptions = { michael@0: allowJavascript: true, michael@0: allowPlugins: true, michael@0: allowAuth: true, michael@0: allowWindowControl: false, michael@0: // Need to override `nodeName` to use `iframe` as `browsers` save session michael@0: // history and in consequence do not dispatch "inner-window-destroyed" michael@0: // notifications. michael@0: browser: false, michael@0: // Note that use of this URL let's use swap frame loaders earlier michael@0: // than if we used default "about:blank". michael@0: uri: "data:text/plain;charset=utf-8," michael@0: }; michael@0: michael@0: let backgroundFrame = createFrame(addonWindow, frameOptions); michael@0: setupPanelFrame(backgroundFrame); michael@0: michael@0: let viewFrame = createFrame(panel, frameOptions); michael@0: setupPanelFrame(viewFrame); michael@0: michael@0: function onDisplayChange({type, target}) { michael@0: // Events from child element like