// -*- tab-width: 2; indent-tabs-mode: nil; js-indent-level: 2 -*-

/* 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/. */

/**
 * PrintUtils is a utility for front-end code to trigger common print
 * operations (printing, show print preview, show page settings).
 *
 * Unfortunately, likely due to inconsistencies in how different operating
 * systems do printing natively, our XPCOM-level printing interfaces
 * are a bit confusing and the method by which we do something basic
 * like printing a page is quite circuitous.
 *
 * To compound that, we need to support remote browsers, and that means
 * kicking off the print jobs in the content process. This means we send
 * messages back and forth to that process via the Printing actor.
 *
 * This also means that <xul:browser>'s that hope to use PrintUtils must have
 * their type attribute set to "content".
 *
 * Messages sent:
 *
 *   Printing:Preview:Enter
 *     This message is sent to put content into print preview mode. We pass
 *     the content window of the browser we're showing the preview of, and
 *     the target of the message is the browser that we'll be showing the
 *     preview in.
 *
 *   Printing:Preview:Exit
 *     This message is sent to take content out of print preview mode.
 */

XPCOMUtils.defineLazyPreferenceGetter(
  this,
  "SHOW_PAGE_SETUP_MENU",
  "print.show_page_setup_menu",
  false
);

XPCOMUtils.defineLazyPreferenceGetter(
  this,
  "PRINT_ALWAYS_SILENT",
  "print.always_print_silent",
  false
);

XPCOMUtils.defineLazyPreferenceGetter(
  this,
  "PREFER_SYSTEM_DIALOG",
  "print.prefer_system_dialog",
  false
);

ChromeUtils.defineESModuleGetters(this, {
  PromptUtils: "resource://gre/modules/PromptUtils.sys.mjs",
});

var PrintUtils = {
  SAVE_TO_PDF_PRINTER: "Mozilla Save to PDF",

  get _bundle() {
    delete this._bundle;
    return (this._bundle = Services.strings.createBundle(
      "chrome://global/locale/printing.properties"
    ));
  },

  async checkForSelection(browsingContext) {
    try {
      let sourceActor =
        browsingContext.currentWindowGlobal.getActor("PrintingSelection");
      // Need the await for the try to trigger...
      return await sourceActor.sendQuery("PrintingSelection:HasSelection", {});
    } catch (e) {
      console.error(e);
    }
    return false;
  },

  /**
   * Updates the hidden state of the "Page Setup" menu items in the File menu,
   * depending on the value of the `print.show_page_setup_menu` pref.
   * Note: not all platforms have a "Page Setup" menu item (or Page Setup
   * window).
   */
  updatePrintSetupMenuHiddenState() {
    let pageSetupMenuItem = document.getElementById("menu_printSetup");
    if (pageSetupMenuItem) {
      pageSetupMenuItem.hidden = !SHOW_PAGE_SETUP_MENU;
    }
  },

  /**
   * Shows the page setup dialog, and saves any settings changed in
   * that dialog if print.save_print_settings is set to true.
   *
   * @return true on success, false on failure
   */
  showPageSetup() {
    let printSettings = this.getPrintSettings();
    // If we come directly from the Page Setup menu, the hack in
    // _enterPrintPreview will not have been invoked to set the last used
    // printer name. For the reasons outlined at that hack, we want that set
    // here too.
    let PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(
      Ci.nsIPrintSettingsService
    );
    if (!PSSVC.lastUsedPrinterName) {
      if (printSettings.printerName) {
        PSSVC.maybeSaveLastUsedPrinterNameToPrefs(printSettings.printerName);
        PSSVC.maybeSavePrintSettingsToPrefs(
          printSettings,
          Ci.nsIPrintSettings.kInitSaveAll
        );
      }
    }
    try {
      var PRINTDIALOGSVC = Cc[
        "@mozilla.org/widget/printdialog-service;1"
      ].getService(Ci.nsIPrintDialogService);
      PRINTDIALOGSVC.showPageSetupDialog(window, printSettings, null);
    } catch (e) {
      dump("showPageSetup " + e + "\n");
      return false;
    }
    return true;
  },

  /**
   * This call exists in a separate method so it can be easily overridden where
   * `gBrowser` doesn't exist (e.g. Thunderbird).
   *
   * @see getTabDialogBox in tabbrowser.js
   */
  getTabDialogBox(sourceBrowser) {
    return gBrowser.getTabDialogBox(sourceBrowser);
  },

  getPreviewBrowser(sourceBrowser) {
    let dialogBox = this.getTabDialogBox(sourceBrowser);
    for (let dialog of dialogBox.getTabDialogManager()._dialogs) {
      let browser = dialog._box.querySelector(".printPreviewBrowser");
      if (browser) {
        return browser;
      }
    }
    return null;
  },

  /**
   * Opens the tab modal version of the print UI for the current tab.
   *
   * @param aBrowsingContext
   *        The BrowsingContext of the window to print.
   * @param aExistingPreviewBrowser
   *        An existing browser created for printing from window.print().
   * @param aPrintSelectionOnly
   *        Whether to print only the active selection of the given browsing
   *        context.
   * @param aPrintFrameOnly
   *        Whether to print the selected frame only
   * @return promise resolving when the dialog is open, rejected if the preview
   *         fails.
   */
  _openTabModalPrint(
    aBrowsingContext,
    aOpenWindowInfo,
    aPrintSelectionOnly,
    aPrintFrameOnly
  ) {
    let sourceBrowser = aBrowsingContext.top.embedderElement;
    let previewBrowser = this.getPreviewBrowser(sourceBrowser);
    if (previewBrowser) {
      // Don't open another dialog if we're already printing.
      //
      // XXX This can be racy can't it? getPreviewBrowser looks at browser that
      // we set up after opening the dialog. But I guess worst case we just
      // open two dialogs so...
      throw new Error("Tab-modal print UI already open");
    }

    // Create the print preview dialog.
    let args = PromptUtils.objectToPropBag({
      printSelectionOnly: !!aPrintSelectionOnly,
      isArticle: sourceBrowser.isArticle,
      printFrameOnly: !!aPrintFrameOnly,
    });
    let dialogBox = this.getTabDialogBox(sourceBrowser);
    let { closedPromise, dialog } = dialogBox.open(
      `chrome://global/content/print.html`,
      { features: "resizable=no", sizeTo: "available" },
      args
    );
    closedPromise.catch(e => {
      console.error(e);
    });

    let settingsBrowser = dialog._frame;
    let printPreview = new PrintPreview({
      sourceBrowsingContext: aBrowsingContext,
      settingsBrowser,
      topBrowsingContext: aBrowsingContext.top,
      activeBrowsingContext: aBrowsingContext,
      openWindowInfo: aOpenWindowInfo,
      printFrameOnly: aPrintFrameOnly,
    });
    // This will create the source browser in connectedCallback() if we sent
    // openWindowInfo. Otherwise the browser will be null.
    settingsBrowser.parentElement.insertBefore(printPreview, settingsBrowser);
    return printPreview.sourceBrowser;
  },

  /**
   * Initialize a print, this will open the tab modal UI if it is enabled or
   * defer to the native dialog/silent print.
   *
   * @param aBrowsingContext
   *        The BrowsingContext of the window to print.
   *        Note that the browsing context could belong to a subframe of the
   *        tab that called window.print, or similar shenanigans.
   * @param aOptions
   *        {windowDotPrintOpenWindowInfo}
   *                              Non-null if this call comes from window.print().
   *                              This is the nsIOpenWindowInfo object that has to
   *                              be passed down to createBrowser in order for the
   *                              static clone that has been cretaed in the child
   *                              process to be linked to the browser it creates
   *                              in the parent process.
   *        {printSelectionOnly}  Whether to print only the active selection of
   *                              the given browsing context.
   *        {printFrameOnly}      Whether to print the selected frame.
   */
  startPrintWindow(aBrowsingContext, aOptions) {
    // At most, one of these is set.
    let { printSelectionOnly, printFrameOnly, windowDotPrintOpenWindowInfo } =
      aOptions || {};

    if (
      windowDotPrintOpenWindowInfo &&
      !windowDotPrintOpenWindowInfo.isForWindowDotPrint
    ) {
      throw new Error("Only expect openWindowInfo for window.print()");
    }

    let browsingContext = aBrowsingContext;
    if (printSelectionOnly) {
      // Ensure that we use the window with focus/selection if the context menu
      // (from which 'Print selection' was selected) happens to have been opened
      // over a different frame.
      let focusedBc = Services.focus.focusedContentBrowsingContext;
      if (
        focusedBc &&
        focusedBc.top.embedderElement == browsingContext.top.embedderElement
      ) {
        browsingContext = focusedBc;
      }
    }

    if (!PRINT_ALWAYS_SILENT && !PREFER_SYSTEM_DIALOG) {
      return this._openTabModalPrint(
        browsingContext,
        windowDotPrintOpenWindowInfo,
        printSelectionOnly,
        printFrameOnly
      );
    }

    const useSystemDialog = PREFER_SYSTEM_DIALOG && !PRINT_ALWAYS_SILENT;

    let browser = null;
    if (windowDotPrintOpenWindowInfo) {
      // When we're called by handleStaticCloneCreatedForPrint(), we must
      // return this browser.
      browser = this.createParentBrowserForStaticClone(
        browsingContext,
        windowDotPrintOpenWindowInfo
      );
      browsingContext = browser.browsingContext;
    }

    // This code is wrapped in an async function so that we can await the async
    // functions that it calls.
    async function makePrintSettingsAndInvokePrint() {
      let settings = PrintUtils.getPrintSettings(
        /*aPrinterName*/ "",
        /*aDefaultsOnly*/ false,
        /*aAllowPseudoPrinter*/ !useSystemDialog
      );
      settings.printSelectionOnly = printSelectionOnly;
      if (
        settings.outputDestination ==
          Ci.nsIPrintSettings.kOutputDestinationFile &&
        !settings.toFileName
      ) {
        // TODO(bug 1748004): We should consider generating the file name
        // from the document's title as we do in print.js's pickFileName
        // (including using DownloadPaths.sanitize!).
        // For now, the following is for consistency with the behavior
        // prior to bug 1669149 part 3.
        let dest = undefined;
        try {
          dest = Services.dirsvc.get("CurWorkD", Ci.nsIFile).path;
        } catch (e) {}
        if (!dest) {
          dest = Services.dirsvc.get("Home", Ci.nsIFile).path;
        }
        settings.toFileName = PathUtils.join(dest, "mozilla.pdf");
      }

      if (useSystemDialog) {
        const hasSelection =
          await PrintUtils.checkForSelection(browsingContext);

        // Prompt the user to choose a printer and make any desired print
        // settings changes.
        let doPrint = false;
        try {
          doPrint = await PrintUtils.handleSystemPrintDialog(
            browsingContext.topChromeWindow,
            hasSelection,
            settings
          );
          if (!doPrint) {
            return;
          }
        } finally {
          // Clean up browser if we aren't going to use it.
          if (!doPrint && browser) {
            browser.remove();
          }
        }
      }

      // At some point we should handle the Promise that this returns (at
      // least report rejection to telemetry).
      browsingContext.print(settings);
    }

    // We need to return to the event loop before calling
    // makePrintSettingsAndInvokePrint() if we were called for `window.print()`.
    // That's because if that function synchronously calls `browser.remove()`
    // or `browsingContext.print()` before we return `browser`, the nested
    // event loop that is being spun in the content process under the
    // OpenInternal call in nsGlobalWindowOuter::Print will still be active.
    // In the case of `browser.remove()`, nsGlobalWindowOuter::Print would then
    // get unhappy once OpenInternal does return since it will fail to return
    // a BrowsingContext. In the case of `browsingContext.print()`, we would
    // re-enter nsGlobalWindowOuter::Print under the nested event loop and
    // printing would then fail since the outer nsGlobalWindowOuter::Print call
    // wouldn't yet have created the static clone.
    setTimeout(makePrintSettingsAndInvokePrint, 0);

    return browser;
  },

  togglePrintPreview(aBrowsingContext) {
    let dialogBox = this.getTabDialogBox(aBrowsingContext.top.embedderElement);
    let dialogs = dialogBox.getTabDialogManager().dialogs;
    let previewDialog = dialogs.find(d =>
      d._box.querySelector(".printSettingsBrowser")
    );
    if (previewDialog) {
      previewDialog.close();
      return;
    }
    this.startPrintWindow(aBrowsingContext);
  },

  /**
   * Called when a content process has created a new BrowsingContext for a
   * static clone of a document that is to be printed, but we do NOT yet have a
   * CanonicalBrowsingContext counterpart in the parent process. This only
   * happens in the following cases:
   *
   *   - content script invoked window.print() in the content process, or:
   *   - silent printing is enabled, and UI code previously invoked
   *     startPrintWindow which called BrowsingContext.print(), and we're now
   *     being called back by the content process to parent the static clone.
   *
   * In the latter case we only need to create the CanonicalBrowsingContext,
   * link it to it's content process counterpart, and inserted it into
   * the document tree; the print in the content process has already been
   * initiated.
   *
   * In the former case we additionally need to check if we should open the
   * tab modal print UI (if not silent printing), obtain a valid
   * nsIPrintSettings object, and tell the content process to initiate the
   * print with this settings object.
   */
  handleStaticCloneCreatedForPrint(aOpenWindowInfo) {
    let browsingContext = aOpenWindowInfo.parent;
    if (aOpenWindowInfo.isForWindowDotPrint) {
      return this.startPrintWindow(browsingContext, {
        windowDotPrintOpenWindowInfo: aOpenWindowInfo,
      });
    }
    return this.createParentBrowserForStaticClone(
      browsingContext,
      aOpenWindowInfo
    );
  },

  createParentBrowserForStaticClone(aBrowsingContext, aOpenWindowInfo) {
    // XXX This code is only called when silent printing, so we're really
    // abusing PrintPreview here. See bug 1768020.
    let printPreview = new PrintPreview({
      sourceBrowsingContext: aBrowsingContext,
      openWindowInfo: aOpenWindowInfo,
    });
    let browser = printPreview.createPreviewBrowser("source");
    document.documentElement.append(browser);
    return browser;
  },

  // "private" methods and members. Don't use them.

  _getErrorCodeForNSResult(nsresult) {
    const MSG_CODES = [
      "GFX_PRINTER_NO_PRINTER_AVAILABLE",
      "GFX_PRINTER_NAME_NOT_FOUND",
      "GFX_PRINTER_COULD_NOT_OPEN_FILE",
      "GFX_PRINTER_STARTDOC",
      "GFX_PRINTER_ENDDOC",
      "GFX_PRINTER_STARTPAGE",
      "GFX_PRINTER_DOC_IS_BUSY",
      "ABORT",
      "NOT_AVAILABLE",
      "NOT_IMPLEMENTED",
      "OUT_OF_MEMORY",
      "UNEXPECTED",
    ];

    for (let code of MSG_CODES) {
      let nsErrorResult = "NS_ERROR_" + code;
      if (Cr[nsErrorResult] == nsresult) {
        return code;
      }
    }

    // PERR_FAILURE is the catch-all error message if we've gotten one that
    // we don't recognize.
    return "FAILURE";
  },

  _displayPrintingError(nsresult, isPrinting, browser) {
    // The nsresults from a printing error are mapped to strings that have
    // similar names to the errors themselves. For example, for error
    // NS_ERROR_GFX_PRINTER_NO_PRINTER_AVAILABLE, the name of the string
    // for the error message is: PERR_GFX_PRINTER_NO_PRINTER_AVAILABLE. What's
    // more, if we're in the process of doing a print preview, it's possible
    // that there are strings specific for print preview for these errors -
    // if so, the names of those strings have _PP as a suffix. It's possible
    // that no print preview specific strings exist, in which case it is fine
    // to fall back to the original string name.
    let msgName = "PERR_" + this._getErrorCodeForNSResult(nsresult);
    let msg, title;
    if (!isPrinting) {
      // Try first with _PP suffix.
      let ppMsgName = msgName + "_PP";
      try {
        msg = this._bundle.GetStringFromName(ppMsgName);
      } catch (e) {
        // We allow localizers to not have the print preview error string,
        // and just fall back to the printing error string.
      }
    }

    if (!msg) {
      msg = this._bundle.GetStringFromName(msgName);
    }

    title = this._bundle.GetStringFromName(
      isPrinting
        ? "print_error_dialog_title"
        : "printpreview_error_dialog_title"
    );

    Services.prompt.asyncAlert(
      browser.browsingContext,
      Services.prompt.MODAL_TYPE_TAB,
      title,
      msg
    );

    Glean.printing.error[this._getErrorCodeForNSResult(nsresult)].add(1);
  },

  getPrintSettings(aPrinterName, aDefaultsOnly, aAllowPseudoPrinter = true) {
    var printSettings;
    try {
      var PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(
        Ci.nsIPrintSettingsService
      );

      function isValidPrinterName(aPrinterName) {
        return (
          aPrinterName &&
          (aAllowPseudoPrinter ||
            aPrinterName != PrintUtils.SAVE_TO_PDF_PRINTER)
        );
      }

      // We must not try to print using an nsIPrintSettings without a printer
      // name set.
      const printerName = (function () {
        if (isValidPrinterName(aPrinterName)) {
          return aPrinterName;
        }
        if (isValidPrinterName(PSSVC.lastUsedPrinterName)) {
          return PSSVC.lastUsedPrinterName;
        }
        return Cc["@mozilla.org/gfx/printerlist;1"].getService(
          Ci.nsIPrinterList
        ).systemDefaultPrinterName;
      })();

      printSettings = PSSVC.createNewPrintSettings();
      printSettings.printerName = printerName;

      // First get any defaults from the printer. We want to skip this for Save
      // to PDF since it isn't a real printer and will throw.
      if (printSettings.printerName != this.SAVE_TO_PDF_PRINTER) {
        PSSVC.initPrintSettingsFromPrinter(
          printSettings.printerName,
          printSettings
        );
      }

      if (!aDefaultsOnly) {
        // Apply any settings that have been saved for this printer.
        PSSVC.initPrintSettingsFromPrefs(
          printSettings,
          true,
          printSettings.kInitSaveAll
        );
      }
    } catch (e) {
      console.error("PrintUtils.getPrintSettings failed:", e);
    }
    return printSettings;
  },

  // Show the system print dialog, saving modified preferences.
  // Returns true if the user clicked print (Not cancel).
  async handleSystemPrintDialog(aWindow, aHasSelection, aSettings) {
    // Prompt the user to choose a printer and make any desired print
    // settings changes.
    try {
      const svc = Cc["@mozilla.org/widget/printdialog-service;1"].getService(
        Ci.nsIPrintDialogService
      );
      await svc.showPrintDialog(aWindow, aHasSelection, aSettings);
    } catch (e) {
      if (e.result == Cr.NS_ERROR_ABORT) {
        return false;
      }
      throw e;
    }

    // Update the saved last used printer name and print settings:
    var PSSVC = Cc["@mozilla.org/gfx/printsettings-service;1"].getService(
      Ci.nsIPrintSettingsService
    );
    PSSVC.maybeSaveLastUsedPrinterNameToPrefs(aSettings.printerName);
    PSSVC.maybeSavePrintSettingsToPrefs(
      aSettings,
      Ci.nsIPrintSettings.kPrintDialogPersistSettings
    );
    return true;
  },
};

/**
 * This class implements a custom element that contains a nested <browser>
 * element. When the user asks to print a document, we create an instance of
 * this class and ask the platform code to create a static clone of the
 * document (a snapshot that won't change due to script running, etc.) in the
 * contained <browser> element.
 *
 * To display a print preview to the user, an instance of this element is added
 * to the tab-modal print preview dialog. As the user changes print preview
 * settings, we may actually end up with multiple instances: one for a preview
 * of the original document, one for a preview of the focused frame, and one
 * for the selected text.
 *
 * To print without displaying a print preview, an instance of this class is
 * appended, hidden, to the end of the top-level chrome browser's document.
 */
class PrintPreview extends MozElements.BaseControl {
  constructor({
    sourceBrowsingContext,
    settingsBrowser,
    topBrowsingContext,
    activeBrowsingContext,
    openWindowInfo,
    printFrameOnly,
  }) {
    super();
    this.sourceBrowsingContext = sourceBrowsingContext;
    this.settingsBrowser = settingsBrowser;
    this.topBrowsingContext = topBrowsingContext;
    this.activeBrowsingContext = activeBrowsingContext;
    this.openWindowInfo = openWindowInfo;
    this.printFrameOnly = printFrameOnly;

    this.printSelectionOnly = false;
    this.simplifyPage = false;
    this.sourceBrowser = null;
    this.selectionBrowser = null;
    this.simplifiedBrowser = null;
    this.lastPreviewBrowser = null;
  }

  connectedCallback() {
    if (this.childElementCount > 0) {
      return;
    }
    this.setAttribute("flex", "1");
    this.append(
      MozXULElement.parseXULToFragment(`
        <stack class="previewStack" rendering="true" flex="1" previewtype="primary">
          <vbox class="previewRendering" flex="1">
            <h1 class="print-pending-label" data-l10n-id="printui-loading"></h1>
          </vbox>
          <html:printpreview-pagination class="printPreviewNavigation"></html:printpreview-pagination>
        </stack>
        <html:link rel="stylesheet" href="chrome://global/content/printPreview.css"/>
    `)
    );
    this.stack = this.firstElementChild;
    this.paginator = this.querySelector("printpreview-pagination");

    if (this.openWindowInfo) {
      // For window.print() we need a browser right away for the contents to be
      // cloned into, create it now.
      this.createPreviewBrowser("source");
    }
  }

  disconnectedCallback() {
    this.exitPrintPreview();
  }

  getSourceBrowsingContext() {
    if (this.openWindowInfo) {
      // If openWindowInfo is set this was for window.print() and the source
      // contents have already been cloned into the preview browser.
      return this.sourceBrowser.browsingContext;
    }
    return this.sourceBrowsingContext;
  }

  get currentBrowsingContext() {
    return this.lastPreviewBrowser.browsingContext;
  }

  exitPrintPreview() {
    this.sourceBrowser?.frameLoader?.exitPrintPreview();
    this.simplifiedBrowser?.frameLoader?.exitPrintPreview();
    this.selectionBrowser?.frameLoader?.exitPrintPreview();

    this.textContent = "";
  }

  async printPreview(settings, { sourceVersion, sourceURI }) {
    this.stack.setAttribute("rendering", true);

    let result = await this._printPreview(settings, {
      sourceVersion,
      sourceURI,
    });

    let browser = this.lastPreviewBrowser;
    this.stack.setAttribute("previewtype", browser.getAttribute("previewtype"));
    browser.setAttribute("sheet-count", result.sheetCount);
    // The view resets to the top of the document on update bug 1686737.
    browser.setAttribute("current-page", 1);
    this.paginator.observePreviewBrowser(browser);
    await document.l10n.translateElements([browser]);

    this.stack.removeAttribute("rendering");

    return result;
  }

  async _printPreview(settings, { sourceVersion, sourceURI }) {
    let printSelectionOnly = sourceVersion == "selection";
    let simplifyPage = sourceVersion == "simplified";
    let selectionTypeBrowser;
    let previewBrowser;

    // Select the existing preview browser elements, these could be null.
    if (printSelectionOnly) {
      selectionTypeBrowser = this.selectionBrowser;
      previewBrowser = this.selectionBrowser;
    } else {
      selectionTypeBrowser = this.sourceBrowser;
      previewBrowser = simplifyPage
        ? this.simplifiedBrowser
        : this.sourceBrowser;
    }

    settings.docURL = sourceURI;

    if (previewBrowser) {
      this.lastPreviewBrowser = previewBrowser;
      if (this.openWindowInfo) {
        // We only want to use openWindowInfo for the window.print() browser,
        // we can get rid of it now.
        this.openWindowInfo = null;
      }
      // This browser has been rendered already, just update it.
      return previewBrowser.frameLoader.printPreview(settings, null);
    }

    if (!selectionTypeBrowser) {
      // Need to create a non-simplified browser.
      selectionTypeBrowser = this.createPreviewBrowser(
        simplifyPage ? "source" : sourceVersion
      );
      let browsingContext =
        printSelectionOnly || this.printFrameOnly
          ? this.activeBrowsingContext
          : this.topBrowsingContext;
      let result = await selectionTypeBrowser.frameLoader.printPreview(
        settings,
        browsingContext
      );
      // If this isn't simplified then we're done.
      if (!simplifyPage) {
        this.lastPreviewBrowser = selectionTypeBrowser;
        return result;
      }
    }

    // We have the base selection/primary browser but need to simplify.
    previewBrowser = this.createPreviewBrowser(sourceVersion);
    await previewBrowser.browsingContext.currentWindowGlobal
      .getActor("Printing")
      .sendQuery("Printing:Preview:ParseDocument", {
        URL: sourceURI,
        windowID:
          selectionTypeBrowser.browsingContext.currentWindowGlobal
            .outerWindowId,
      });

    // We've parsed a simplified version into the preview browser. Convert that to
    // a print preview as usual.
    this.lastPreviewBrowser = previewBrowser;
    return previewBrowser.frameLoader.printPreview(
      settings,
      previewBrowser.browsingContext
    );
  }

  createPreviewBrowser(sourceVersion) {
    let browser = document.createXULElement("browser");
    let browsingContext =
      sourceVersion == "selection" ||
      this.printFrameOnly ||
      (sourceVersion == "source" && this.openWindowInfo)
        ? this.sourceBrowsingContext
        : this.sourceBrowsingContext.top;
    if (sourceVersion == "source" && this.openWindowInfo) {
      browser.openWindowInfo = this.openWindowInfo;
    } else {
      let userContextId = browsingContext.originAttributes.userContextId;
      if (userContextId) {
        browser.setAttribute("usercontextid", userContextId);
      }
      browser.setAttribute(
        "initialBrowsingContextGroupId",
        browsingContext.group.id
      );
    }
    browser.setAttribute("type", "content");
    let remoteType = browsingContext.currentRemoteType;
    if (remoteType) {
      browser.setAttribute("remoteType", remoteType);
      browser.setAttribute("remote", "true");
    }
    // When the print process finishes, we get closed by
    // nsDocumentViewer::OnDonePrinting, or by the print preview code.
    //
    // When that happens, we should remove us from the DOM if connected.
    browser.addEventListener("DOMWindowClose", function (e) {
      if (this.isConnected) {
        this.remove();
      }
      e.stopPropagation();
      e.preventDefault();
    });

    if (this.settingsBrowser) {
      browser.addEventListener("contextmenu", function (e) {
        e.preventDefault();
      });

      browser.setAttribute("previewtype", sourceVersion);
      browser.classList.add("printPreviewBrowser");
      browser.setAttribute("flex", "1");
      browser.setAttribute("printpreview", "true");
      browser.setAttribute("nodefaultsrc", "true");
      document.l10n.setAttributes(browser, "printui-preview-label");

      this.stack.insertBefore(browser, this.paginator);

      if (sourceVersion == "source") {
        this.sourceBrowser = browser;
      } else if (sourceVersion == "selection") {
        this.selectionBrowser = browser;
      } else if (sourceVersion == "simplified") {
        this.simplifiedBrowser = browser;
      }
    } else {
      browser.style.visibility = "collapse";
    }
    return browser;
  }
}
customElements.define("print-preview", PrintPreview);
