Electron のポップアップウィンドウでコピペできるようにする

あったかくなって気持ちいいですね。先日新しいテレビを買ったらHDMI端子が多くてちょっと嬉しかった亀山です。 Electron で作っているアプリではメニューをカスタマイズしているのですが、ログイン用のポップアップウィンドウでコピペができなくなって困ったという話です。そこで macOS 向けにウィンドウごとにメニューを切り替えることでコピペできるようにする方法について説明します。

メニューのカスタマイズ

Menu.setApplicationMenu を使ってアプリ全体のメニューをカスタマイズすることができます。私が作ったアプリでは Cmd+C などのコピー・ペースト周りを自前で実装するためカスタマイズを行っていました。

ポップアップ

Electron では新しい Window を開くときの挙動をメインプロセス側で指示することができます。以下のコードで特定の URL (今回は認証画面の URL) を開いたときだけ Window を開くことを許可します。

mainWindow.webContents.setWindowOpenHandler(({ url }) => {
  if (url.startsWith("http://<ポップアップしたいURL>")) {
    return { action: "allow" }
  }
  return { action: "deny" }
})

BrowserWindow.setMenu

パスワードマネージャを使っているユーザーのために、ログイン画面ではパスワードのペーストを許可したいです。ところが、メニューをカスタマイズしているためコピー・ペーストが使えなくなっています。これは、キーボードショートカットの機能がメニューによって決定されるためです。

そこで、ウィンドウごとにメニューを指定できる BrowserWindow.setMenu があるのですが、これはウィンドウ上部にメニューバーがくっついている OS、Windows と Linux だけで利用できる機能です。macOS はアプリでひとつのメニューバーしかもつことができません。

browser-window-focus イベント

そこでウィンドウが切り替わったタイミングで Menu.setApplicationMenu を呼び、メニューを切り替えることにします。ウィンドウにフォーカスが当たったときに browser-window-focus が発行されるので、次のように書くことが出来ます。今回は、メインウィンドウはカスタマイズしたメニューを、そうでなければ標準のメニューを表示することにします。標準のメニューにはコピー・ペーストの項目が入っているため、ポップアップウィンドウでコピー・ペーストが使えるようになります。

app.on("browser-window-focus", (event, window) => {
  const defaultMenu = Menu.buildFromTemplate(defaultMenuTemplate)
  Menu.setApplicationMenu(window === mainWindow ? mainMenu : defaultMenu)
})

mainWindow と mainMenu はメインウィンドウ生成時に変数に格納しておきます。

let mainWindow: BrowserWindow
let mainMenu: Electron.Menu

const createWindow = () => {
  mainWindow = new BrowserWindow({
    ...
  })

  mainMenu = Menu.buildFromTemplate(
    ...
  )
  Menu.setApplicationMenu(mainMenu)
}

app.on("ready", createWindow)

デフォルトのメニュー

デフォルトのメニューはドキュメントを参考に下記のようになります。

www.electronjs.org

TypeScript で型をつけるために、やや乱暴ですが as MenuItemConstructorOptions[] を付加しています。

const defaultMenuTemplate = [
  // { role: 'appMenu' }
  ...(isMac
    ? [
        {
          label: app.name,
          submenu: [
            { role: "about" },
            { type: "separator" },
            { role: "services" },
            { type: "separator" },
            { role: "hide" },
            { role: "hideOthers" },
            { role: "unhide" },
            { type: "separator" },
            { role: "quit" },
          ],
        },
      ]
    : []),
  // { role: 'fileMenu' }
  {
    label: "File",
    submenu: [isMac ? { role: "close" } : { role: "quit" }],
  },
  // { role: 'editMenu' }
  {
    label: "Edit",
    submenu: [
      { role: "undo" },
      { role: "redo" },
      { type: "separator" },
      { role: "cut" },
      { role: "copy" },
      { role: "paste" },
      ...(isMac
        ? [
            { role: "pasteAndMatchStyle" },
            { role: "delete" },
            { role: "selectAll" },
            { type: "separator" },
            {
              label: "Speech",
              submenu: [{ role: "startSpeaking" }, { role: "stopSpeaking" }],
            },
          ]
        : [{ role: "delete" }, { type: "separator" }, { role: "selectAll" }]),
    ],
  },
  // { role: 'viewMenu' }
  {
    label: "View",
    submenu: [
      { role: "reload" },
      { role: "forceReload" },
      { role: "toggleDevTools" },
      { type: "separator" },
      { role: "resetZoom" },
      { role: "zoomIn" },
      { role: "zoomOut" },
      { type: "separator" },
      { role: "togglefullscreen" },
    ],
  },
  // { role: 'windowMenu' }
  {
    label: "Window",
    submenu: [
      { role: "minimize" },
      { role: "zoom" },
      ...(isMac
        ? [
            { type: "separator" },
            { role: "front" },
            { type: "separator" },
            { role: "window" },
          ]
        : [{ role: "close" }]),
    ],
  },
  {
    role: "help",
    submenu: [
      {
        label: "Learn More",
        click: async () => {
          const { shell } = require("electron")
          await shell.openExternal("https://electronjs.org")
        },
      },
    ],
  },
] as MenuItemConstructorOptions[]

おわりに

macOS 向けにウィンドウごとにメニューを切り替える方法について説明しました。実際には Windows では BrowserWindow.setMenu を使うように実行プラットフォームによって対応を変えるコードが必要になるかと思います。