玩转electron context menu


electron有提供context menu的事件,但是menu需要我们自己去创建,可以实现一些自定义的功能,还有就是i18n也是在创建过程中可以去实现。

创建简单的context menu

我们先来实现最简单的copy/paste/cut的三个context menu的功能,从这三个最简单的menu开始入手。

先创建一个mainWindow,然后在mainWindow的webContents上面监听context-menu事件,然后在事件中创建menu,最后调用menu.popup()方法弹出menu。copy/paste/cut是electron已经实现的功能,我们只需要调用相应的role就可以了, 不需要自己再去实现一次click事件。

// Modules to control application life and create native browser window
const { app, BrowserWindow, Menu } = require('electron')
const path = require('node:path')

function createWindow () {
  // Create the browser window.
  const mainWindow = new BrowserWindow({
    width: 800,
    height: 600,
    webPreferences: {
      preload: path.join(__dirname, 'preload.js')
    }
  })

  // and load the index.html of the app.
  mainWindow.loadFile('index.html')
  return mainWindow

  // Open the DevTools.
  // mainWindow.webContents.openDevTools()
}

// This method will be called when Electron has finished
// initialization and is ready to create browser windows.
// Some APIs can only be used after this event occurs.
app.whenReady().then(() => {
  const m = createWindow()
  m.webContents.on("context-menu",(_event, props) => {
    const { editFlags } = props;
    const template = [
      {
        id: 'cut',
        label: 'Cut',
        role: 'cut',
        enabled: editFlags.canCut,
      },
      {
        id: 'copy',
        label: 'Copy',
        role: 'copy',
        enabled: editFlags.canCopy,
      },
      {
        id: 'paste',
        label: 'Paste',
        role: 'paste',
        enabled: editFlags.canPaste,
      }
    ];

    const menu = Menu.buildFromTemplate(template);
    menu.popup({});
  })

  app.on('activate', function () {
    // On macOS it's common to re-create a window in the app when the
    // dock icon is clicked and there are no other windows open.
    if (BrowserWindow.getAllWindows().length === 0) createWindow()
  })
})

// Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q.
app.on('window-all-closed', function () {
  if (process.platform !== 'darwin') app.quit()
})

// In this file you can include the rest of your app's specific main process
// code. You can also put them in separate files and require them here.

这样我们就实现了最简单的copy/paste/cut的context menu功能, 效果如下图:

context menu 1

import { MenuItemConstructorOptions, Menu, clipboard, app } from 'electron';
class ContextMenuController {
  private createDictionarySuggestions(
    properties: SpellCheckProperties,
    mainWindow: Electron.BrowserWindow,
  ): MenuItemConstructorOptions[] {
    const hasText = properties.selectionText.length > 0;

    if (!hasText || !properties.misspelledWord) {
      return [];
    }

    if (properties.dictionarySuggestions.length === 0) {
      return [
        {
          id: 'dictionarySuggestions',
          label: 'No Guesses Found',
          visible: true,
          enabled: false,
        },
      ];
    }

    return properties.dictionarySuggestions.map((suggestion) => ({
      id: 'dictionarySuggestions',
      label: suggestion,
      visible: Boolean(
        properties.isEditable && hasText && properties.misspelledWord,
      ),
      click: (menuItem: Electron.MenuItem) => {
        mainWindow.webContents.replaceMisspelling(menuItem.label);
      },
    }));
  }

  private createEditMenuItems(
    properties: SpellCheckProperties,
  ): MenuItemConstructorOptions[] {
    const hasText = properties.selectionText.trim().length > 0;
    const can = (type: string) => properties.editFlags[`can${type}`] && hasText;

    const template: MenuItemConstructorOptions[] = [
      {
        id: 'cut',
        label: 'Cu&t',
        role: 'cut',
        enabled: can('Cut'),
        visible: properties.isEditable,
      },
      {
        id: 'copy',
        label: '&Copy',
        role: 'copy',
        enabled: can('Copy'),
        visible: properties.isEditable || hasText,
      },
      {
        id: 'paste',
        label: '&Paste',
        role: 'paste',
        enabled: properties.editFlags.canPaste,
        visible: properties.isEditable,
      },
    ];

    // remove role from items that are not enabled
    // https://github.com/electron/electron/issues/13554
    template.forEach((item) => {
      if (item.enabled === false) {
        item.role = undefined;
      }
    });

    return template;
  }

  private createSpellCheckMenuItem(
    properties: SpellCheckProperties,
    mainWindow: Electron.BrowserWindow,
  ): MenuItemConstructorOptions {
    const hasText = properties.selectionText.length > 0;

    return {
      id: 'learnSpelling',
      label: '&Learn Spelling',
      visible: Boolean(
        properties.isEditable && hasText && properties.misspelledWord,
      ),
      click: () => {
        mainWindow.webContents.session.addWordToSpellCheckerDictionary(
          properties.misspelledWord,
        );
      },
    };
  }

  private createLinkMenuItem(
    properties: SpellCheckProperties,
  ): MenuItemConstructorOptions {
    return {
      id: 'copyLink',
      label: 'Copy Lin&k',
      visible: properties.linkURL.length > 0 && properties.mediaType === 'none',
      click: () => {
        clipboard.write({
          bookmark: properties.linkText,
          text: properties.linkURL,
        });
      },
    };
  }

  private removeInvisibleItems(
    template: MenuItemConstructorOptions[],
  ): MenuItemConstructorOptions[] {
    const filtered = template.filter((item) => item.visible !== false);

    return filtered.reduce((acc, curr, index, arr) => {
      if (index === 0 && curr.type === 'separator') {
        return acc;
      }

      if (curr.type === 'separator' && arr[index - 1]?.type === 'separator') {
        return acc;
      }

      if (index === arr.length - 1 && curr.type === 'separator') {
        return acc;
      }

      acc.push(curr);
      return acc;
    }, [] as MenuItemConstructorOptions[]);
  }

  private contextMenu(w: Electron.BrowserWindow) {
    w.webContents.on('context-menu', (_event, properties) => {
      const template: MenuItemConstructorOptions[] = [
        ...this.createDictionarySuggestions(properties, w),
        { type: 'separator' },
        this.createSpellCheckMenuItem(properties, w),
        { type: 'separator' },
        ...this.createEditMenuItems(properties),
        this.createLinkMenuItem(properties),
      ];

      const filteredTemplate = this.removeInvisibleItems(template);
      if (filteredTemplate.length > 0) {
        const menu = Menu.buildFromTemplate(filteredTemplate);
        menu.popup({});
      }
    });
  }

  init(mainWindow: Electron.BrowserWindow) {
    // create context menu for main window
    this.contextMenu(mainWindow);

    // creqte context menu for all new windows
    app.on('browser-window-created', (_event, window) => {
      this.contextMenu(window);
    });
  };
}

export { ContextMenuController };

文章作者: Payne Fu
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Payne Fu !
评论
  目录