Remove usage of Electron context menus

Summary: Removed the usage of electron's native context menus, and replaces it with Antd based context menu's.

Reviewed By: passy

Differential Revision: D31990756

fbshipit-source-id: 0312cbac5fd20a1a30603ce1058c03f4291b23b1
This commit is contained in:
Michel Weststrate
2021-10-28 07:26:45 -07:00
committed by Facebook GitHub Bot
parent 25590e14b9
commit 64e791e253
11 changed files with 156 additions and 185 deletions

View File

@@ -35,6 +35,7 @@ import {textContent} from 'flipper-plugin';
import createPaste from './fb-stubs/createPaste'; import createPaste from './fb-stubs/createPaste';
import {getPluginTitle} from './utils/pluginUtils'; import {getPluginTitle} from './utils/pluginUtils';
import {getFlipperLib} from 'flipper-plugin'; import {getFlipperLib} from 'flipper-plugin';
import {ContextMenuItem} from './ui/components/ContextMenu';
type OwnProps = { type OwnProps = {
onClear: () => void; onClear: () => void;
@@ -421,7 +422,7 @@ class NotificationItem extends Component<
> { > {
constructor(props: ItemProps & PluginNotification) { constructor(props: ItemProps & PluginNotification) {
super(props); super(props);
const items: Array<Electron.MenuItemConstructorOptions> = []; const items: Array<ContextMenuItem> = [];
if (props.onHidePlugin && props.plugin) { if (props.onHidePlugin && props.plugin) {
items.push({ items.push({
label: `Hide ${getPluginTitle(props.plugin)} plugin`, label: `Hide ${getPluginTitle(props.plugin)} plugin`,
@@ -444,7 +445,7 @@ class NotificationItem extends Component<
} }
state = {reportedNotHelpful: false}; state = {reportedNotHelpful: false};
contextMenuItems: Array<Electron.MenuItemConstructorOptions>; contextMenuItems: Array<ContextMenuItem>;
deepLinkButton = React.createRef(); deepLinkButton = React.createRef();
createPaste = () => { createPaste = () => {

View File

@@ -57,7 +57,7 @@ exports[`load PluginInstaller list 1`] = `
tabindex="0" tabindex="0"
> >
<div <div
class="css-18abd42-View-FlexBox-FlexColumn ecr18to0" class="ant-dropdown-trigger css-18abd42-View-FlexBox-FlexColumn ecr18to0"
> >
<div <div
class="css-1otvu18-View-FlexBox-FlexRow-TableHeadContainer ejga3101" class="css-1otvu18-View-FlexBox-FlexRow-TableHeadContainer ejga3101"
@@ -132,7 +132,7 @@ exports[`load PluginInstaller list 1`] = `
class="css-p5h61d-View-FlexBox-FlexColumn-Container esta8x30" class="css-p5h61d-View-FlexBox-FlexColumn-Container esta8x30"
> >
<div <div
class="css-18abd42-View-FlexBox-FlexColumn ecr18to0" class="ant-dropdown-trigger css-18abd42-View-FlexBox-FlexColumn ecr18to0"
> >
<div <div
class="css-hg3ptm-View-FlexBox-FlexRow-TableBodyRowContainer ehuguum1" class="css-hg3ptm-View-FlexBox-FlexRow-TableBodyRowContainer ehuguum1"
@@ -391,7 +391,7 @@ exports[`load PluginInstaller list with one plugin installed 1`] = `
tabindex="0" tabindex="0"
> >
<div <div
class="css-18abd42-View-FlexBox-FlexColumn ecr18to0" class="ant-dropdown-trigger css-18abd42-View-FlexBox-FlexColumn ecr18to0"
> >
<div <div
class="css-1otvu18-View-FlexBox-FlexRow-TableHeadContainer ejga3101" class="css-1otvu18-View-FlexBox-FlexRow-TableHeadContainer ejga3101"
@@ -466,7 +466,7 @@ exports[`load PluginInstaller list with one plugin installed 1`] = `
class="css-p5h61d-View-FlexBox-FlexColumn-Container esta8x30" class="css-p5h61d-View-FlexBox-FlexColumn-Container esta8x30"
> >
<div <div
class="css-18abd42-View-FlexBox-FlexColumn ecr18to0" class="ant-dropdown-trigger css-18abd42-View-FlexBox-FlexColumn ecr18to0"
> >
<div <div
class="css-hg3ptm-View-FlexBox-FlexRow-TableBodyRowContainer ehuguum1" class="css-hg3ptm-View-FlexBox-FlexRow-TableBodyRowContainer ehuguum1"

View File

@@ -10,7 +10,6 @@
import {Provider} from 'react-redux'; import {Provider} from 'react-redux';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import ContextMenuProvider from './ui/components/ContextMenuProvider';
import GK from './fb-stubs/GK'; import GK from './fb-stubs/GK';
import {init as initLogger} from './fb-stubs/Logger'; import {init as initLogger} from './fb-stubs/Logger';
import {SandyApp} from './sandy-chrome/SandyApp'; import {SandyApp} from './sandy-chrome/SandyApp';
@@ -137,11 +136,9 @@ class AppFrame extends React.Component<
<PersistGate persistor={persistor}> <PersistGate persistor={persistor}>
<CacheProvider value={cache}> <CacheProvider value={cache}>
<TooltipProvider> <TooltipProvider>
<ContextMenuProvider> <_NuxManagerContext.Provider value={_createNuxManager()}>
<_NuxManagerContext.Provider value={_createNuxManager()}> <SandyApp />
<SandyApp /> </_NuxManagerContext.Provider>
</_NuxManagerContext.Provider>
</ContextMenuProvider>
</TooltipProvider> </TooltipProvider>
</CacheProvider> </CacheProvider>
</PersistGate> </PersistGate>

View File

@@ -148,48 +148,50 @@ export function SandyApp() {
) : null; ) : null;
return ( return (
<Layout.Bottom> <RootElement>
<Layout.Left> <Layout.Bottom>
<Layout.Horizontal> <Layout.Left>
<LeftRail <Layout.Horizontal>
toplevelSelection={toplevelSelection} <LeftRail
setToplevelSelection={setToplevelSelection} toplevelSelection={toplevelSelection}
/> setToplevelSelection={setToplevelSelection}
<_Sidebar width={250} minWidth={220} maxWidth={800} gutter> />
{leftMenuContent && ( <_Sidebar width={250} minWidth={220} maxWidth={800} gutter>
<TrackingScope scope={toplevelSelection!}> {leftMenuContent && (
{leftMenuContent} <TrackingScope scope={toplevelSelection!}>
</TrackingScope> {leftMenuContent}
)} </TrackingScope>
</_Sidebar>
</Layout.Horizontal>
<MainContainer>
{staticView ? (
<TrackingScope
scope={
(staticView as any).displayName ??
staticView.name ??
staticView.constructor?.name ??
'unknown static view'
}>
{staticView === WelcomeScreenStaticView ? (
React.createElement(staticView) /* avoid shadow */
) : (
<ContentContainer>
{React.createElement(staticView, {
logger: logger,
})}
</ContentContainer>
)} )}
</TrackingScope> </_Sidebar>
) : ( </Layout.Horizontal>
<PluginContainer logger={logger} /> <MainContainer>
)} {staticView ? (
{outOfContentsContainer} <TrackingScope
</MainContainer> scope={
</Layout.Left> (staticView as any).displayName ??
<_PortalsManager /> staticView.name ??
</Layout.Bottom> staticView.constructor?.name ??
'unknown static view'
}>
{staticView === WelcomeScreenStaticView ? (
React.createElement(staticView) /* avoid shadow */
) : (
<ContentContainer>
{React.createElement(staticView, {
logger: logger,
})}
</ContentContainer>
)}
</TrackingScope>
) : (
<PluginContainer logger={logger} />
)}
{outOfContentsContainer}
</MainContainer>
</Layout.Left>
<_PortalsManager />
</Layout.Bottom>
</RootElement>
); );
} }
@@ -220,6 +222,12 @@ const MainContainer = styled(Layout.Container)({
padding: `${theme.space.large}px ${theme.space.large}px ${theme.space.large}px 0`, padding: `${theme.space.large}px ${theme.space.large}px ${theme.space.large}px 0`,
}); });
const RootElement = styled.div({
display: 'flex',
height: '100%',
});
RootElement.displayName = 'SandyAppRootElement';
function registerStartupTime(logger: Logger) { function registerStartupTime(logger: Logger) {
// track time since launch // track time since launch
const [s, ns] = process.hrtime(); const [s, ns] = process.hrtime();

View File

@@ -7,19 +7,30 @@
* @format * @format
*/ */
import { import * as React from 'react';
createElement, import {Menu, Dropdown} from 'antd';
useContext, import {createElement, useCallback, forwardRef, Ref, ReactElement} from 'react';
useCallback,
forwardRef,
Ref,
ReactElement,
} from 'react';
import {ContextMenuContext} from './ContextMenuProvider';
import FlexColumn from './FlexColumn'; import FlexColumn from './FlexColumn';
import {MenuItemConstructorOptions} from 'electron'; import {CheckOutlined} from '@ant-design/icons';
export type MenuTemplate = Array<MenuItemConstructorOptions>; export type ContextMenuItem =
| {
readonly label: string;
readonly click?: () => void;
readonly role?: string;
readonly enabled?: boolean;
readonly type?: 'normal' | 'checkbox';
readonly checked?: boolean;
}
| {
readonly type: 'separator';
}
| {
readonly label: string;
readonly submenu: MenuTemplate;
};
export type MenuTemplate = ReadonlyArray<ContextMenuItem>;
type Props<C> = { type Props<C> = {
/** List of items in the context menu. Used for static menus. */ /** List of items in the context menu. Used for static menus. */
@@ -33,6 +44,8 @@ type Props<C> = {
onMouseDown?: (e: React.MouseEvent) => any; onMouseDown?: (e: React.MouseEvent) => any;
} & C; } & C;
const contextMenuTrigger = ['contextMenu' as const];
/** /**
* Native context menu that is shown on secondary click. * Native context menu that is shown on secondary click.
* Uses [Electron's context menu API](https://electronjs.org/docs/api/menu-item) * Uses [Electron's context menu API](https://electronjs.org/docs/api/menu-item)
@@ -45,21 +58,53 @@ export default forwardRef(function ContextMenu<C>(
{items, buildItems, component, children, ...otherProps}: Props<C>, {items, buildItems, component, children, ...otherProps}: Props<C>,
ref: Ref<any> | null, ref: Ref<any> | null,
) { ) {
const contextMenuManager = useContext(ContextMenuContext);
const onContextMenu = useCallback(() => { const onContextMenu = useCallback(() => {
if (items != null) { return createContextMenu(items ?? buildItems?.() ?? []);
contextMenuManager?.appendToContextMenu(items);
} else if (buildItems != null) {
contextMenuManager?.appendToContextMenu(buildItems());
}
}, [items, buildItems]); }, [items, buildItems]);
return createElement(
component || FlexColumn, return (
{ <Dropdown overlay={onContextMenu} trigger={contextMenuTrigger}>
ref, {createElement(
onContextMenu, component || FlexColumn,
...otherProps, {
}, ref,
children, ...otherProps,
},
children,
)}
</Dropdown>
); );
}) as <T>(p: Props<T> & {ref?: Ref<any>}) => ReactElement; }) as <T>(p: Props<T> & {ref?: Ref<any>}) => ReactElement;
export function createContextMenu(items: MenuTemplate) {
return <Menu>{items.map(createMenuItem)}</Menu>;
}
function createMenuItem(item: ContextMenuItem, idx: number) {
if ('type' in item && item.type === 'separator') {
return <Menu.Divider key={idx} />;
} else if ('submenu' in item) {
return (
<Menu.SubMenu key={idx} title={item.label}>
{item.submenu.map(createMenuItem)}
</Menu.SubMenu>
);
} else if ('label' in item) {
return (
<Menu.Item
key={idx}
onClick={item.click}
disabled={item.enabled === false}
role={item.role}
icon={
item.type === 'checkbox' ? (
<CheckOutlined
style={{visibility: item.checked ? 'visible' : 'hidden'}}
/>
) : undefined
}>
{item.label}
</Menu.Item>
);
}
}

View File

@@ -1,58 +0,0 @@
/**
* Copyright (c) Facebook, Inc. and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
*/
import styled from '@emotion/styled';
import electron, {MenuItemConstructorOptions} from 'electron';
import React, {useRef, memo, createContext, useMemo, useCallback} from 'react';
type MenuTemplate = Array<MenuItemConstructorOptions>;
interface ContextMenuManager {
appendToContextMenu(items: MenuTemplate): void;
}
const Container = styled.div({
display: 'flex',
height: '100%',
});
Container.displayName = 'ContextMenuProvider:Container';
export const ContextMenuContext = createContext<ContextMenuManager | undefined>(
undefined,
);
/**
* Flipper's root is already wrapped with this component, so plugins should not
* need to use this. ContextMenu is what you probably want to use.
* @deprecated use https://ant.design/components/dropdown/#components-dropdown-demo-context-menu
*/
const ContextMenuProvider: React.FC<{}> = memo(function ContextMenuProvider({
children,
}) {
const menuTemplate = useRef<MenuTemplate>([]);
const contextMenuManager = useMemo(
() => ({
appendToContextMenu(items: MenuTemplate) {
menuTemplate.current = menuTemplate.current.concat(items);
},
}),
[],
);
const onContextMenu = useCallback(() => {
const menu = electron.remote.Menu.buildFromTemplate(menuTemplate.current);
menuTemplate.current = [];
menu.popup({window: electron.remote.getCurrentWindow()});
}, []);
return (
<ContextMenuContext.Provider value={contextMenuManager}>
<Container onContextMenu={onContextMenu}>{children}</Container>
</ContextMenuContext.Provider>
);
});
export default ContextMenuProvider;

View File

@@ -11,24 +11,22 @@ import {Filter} from '../filter/types';
import {PureComponent} from 'react'; import {PureComponent} from 'react';
import Text from '../Text'; import Text from '../Text';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import electron, {MenuItemConstructorOptions} from 'electron';
import React from 'react'; import React from 'react';
import {Property} from 'csstype'; import {Property} from 'csstype';
import {theme} from 'flipper-plugin'; import {theme} from 'flipper-plugin';
import {ContextMenuItem, createContextMenu} from '../ContextMenu';
import {Dropdown} from 'antd';
const Token = styled(Text)<{focused?: boolean; color?: Property.Color}>( const Token = styled(Text)<{focused?: boolean; color?: Property.Color}>(
(props) => ({ (props) => ({
display: 'inline-flex', display: 'inline-flex',
alignItems: 'center', alignItems: 'center',
backgroundColor: props.focused backgroundColor: props.color || theme.buttonDefaultBackground,
? theme.textColorActive
: props.color || theme.buttonDefaultBackground,
borderRadius: 4, borderRadius: 4,
marginRight: 4, marginRight: 4,
padding: 4, padding: 4,
paddingLeft: 6, paddingLeft: 6,
height: 21, height: 21,
color: props.focused ? 'white' : 'inherit',
'&:active': { '&:active': {
backgroundColor: theme.textColorActive, backgroundColor: theme.textColorActive,
color: theme.textColorPrimary, color: theme.textColorPrimary,
@@ -103,8 +101,6 @@ type Props = {
}; };
export default class FilterToken extends PureComponent<Props> { export default class FilterToken extends PureComponent<Props> {
_ref?: Element | null;
onMouseDown = () => { onMouseDown = () => {
if ( if (
this.props.filter.type !== 'enum' || this.props.filter.type !== 'enum' ||
@@ -117,7 +113,7 @@ export default class FilterToken extends PureComponent<Props> {
}; };
showDetails = () => { showDetails = () => {
const menuTemplate: Array<MenuItemConstructorOptions> = []; const menuTemplate: Array<ContextMenuItem> = [];
if (this.props.filter.type === 'enum') { if (this.props.filter.type === 'enum') {
menuTemplate.push( menuTemplate.push(
@@ -155,19 +151,7 @@ export default class FilterToken extends PureComponent<Props> {
}, },
); );
} }
const menu = electron.remote.Menu.buildFromTemplate(menuTemplate); return createContextMenu(menuTemplate);
if (this._ref) {
const {bottom, left} = this._ref.getBoundingClientRect();
menu.popup({
window: electron.remote.getCurrentWindow(),
// @ts-ignore: async is private API
async: true,
// Note: Electron requires the x/y parameters to be integer values for marshalling
x: Math.round(left),
y: Math.round(bottom + 8),
});
}
}; };
toggleFilter = () => { toggleFilter = () => {
@@ -202,10 +186,6 @@ export default class FilterToken extends PureComponent<Props> {
} }
}; };
setRef = (ref: HTMLSpanElement | null) => {
this._ref = ref;
};
render() { render() {
const {filter} = this.props; const {filter} = this.props;
let color; let color;
@@ -231,21 +211,24 @@ export default class FilterToken extends PureComponent<Props> {
} }
return ( return (
<Token <Dropdown trigger={dropdownTrigger} overlay={this.showDetails}>
key={`${filter.key}:${value}=${filter.type}`} <Token
tabIndex={-1} key={`${filter.key}:${value}=${filter.type}`}
onMouseDown={this.onMouseDown} tabIndex={-1}
focused={this.props.focused} onMouseDown={this.onMouseDown}
color={color} focused={this.props.focused}
ref={this.setRef}> color={color}>
<Key type={this.props.filter.type} focused={this.props.focused}> <Key type={this.props.filter.type} focused={this.props.focused}>
{filter.key} {filter.key}
</Key> </Key>
<Value>{value}</Value> <Value>{value}</Value>
<Chevron tabIndex={-1} focused={this.props.focused}> <Chevron tabIndex={-1} focused={this.props.focused}>
&#8964; &#8964;
</Chevron> </Chevron>
</Token> </Token>
</Dropdown>
); );
} }
} }
const dropdownTrigger = ['click' as const];

View File

@@ -17,12 +17,11 @@ import {
TableBodyRow, TableBodyRow,
TableOnAddFilter, TableOnAddFilter,
} from './types'; } from './types';
import {MenuTemplate} from '../ContextMenu'; import {ContextMenuItem, MenuTemplate} from '../ContextMenu';
import React from 'react'; import React from 'react';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import AutoSizer from 'react-virtualized-auto-sizer'; import AutoSizer from 'react-virtualized-auto-sizer';
import {VariableSizeList as List} from 'react-window'; import {VariableSizeList as List} from 'react-window';
import {MenuItemConstructorOptions} from 'electron';
import TableHead from './TableHead'; import TableHead from './TableHead';
import TableRow from './TableRow'; import TableRow from './TableRow';
import ContextMenu from '../ContextMenu'; import ContextMenu from '../ContextMenu';
@@ -523,7 +522,7 @@ export class ManagedTable extends React.Component<
getFlipperLib().writeTextToClipboard(cellText); getFlipperLib().writeTextToClipboard(cellText);
}; };
buildContextMenuItems: () => Array<MenuItemConstructorOptions> = () => { buildContextMenuItems: () => Array<ContextMenuItem> = () => {
const {highlightedRows} = this.state; const {highlightedRows} = this.state;
if (highlightedRows.size === 0) { if (highlightedRows.size === 0) {
return []; return [];

View File

@@ -17,13 +17,12 @@ import {
} from './types'; } from './types';
import {normalizeColumnWidth, isPercentage} from './utils'; import {normalizeColumnWidth, isPercentage} from './utils';
import {PureComponent} from 'react'; import {PureComponent} from 'react';
import ContextMenu from '../ContextMenu'; import ContextMenu, {ContextMenuItem} from '../ContextMenu';
import {theme, _Interactive, _InteractiveProps} from 'flipper-plugin'; import {theme, _Interactive, _InteractiveProps} from 'flipper-plugin';
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import {colors} from '../colors'; import {colors} from '../colors';
import FlexRow from '../FlexRow'; import FlexRow from '../FlexRow';
import invariant from 'invariant'; import invariant from 'invariant';
import {MenuItemConstructorOptions} from 'electron';
import React from 'react'; import React from 'react';
const TableHeaderArrow = styled.span({ const TableHeaderArrow = styled.span({
@@ -208,7 +207,7 @@ export default class TableHead extends PureComponent<{
onColumnResize?: TableOnColumnResize; onColumnResize?: TableOnColumnResize;
horizontallyScrollable?: boolean; horizontallyScrollable?: boolean;
}> { }> {
buildContextMenu = (): MenuItemConstructorOptions[] => { buildContextMenu = (): ContextMenuItem[] => {
const visibles = this.props.columnOrder const visibles = this.props.columnOrder
.map((c) => (c.visible ? c.key : null)) .map((c) => (c.visible ? c.key : null))
.filter(Boolean) .filter(Boolean)

View File

@@ -60,7 +60,6 @@ export {default as Orderable} from './components/Orderable';
export {Component, PureComponent} from 'react'; export {Component, PureComponent} from 'react';
// context menus and dropdowns // context menus and dropdowns
export {default as ContextMenuProvider} from './components/ContextMenuProvider';
export {default as ContextMenu} from './components/ContextMenu'; export {default as ContextMenu} from './components/ContextMenu';
// file // file

View File

@@ -8,8 +8,6 @@
*/ */
import fs from 'fs'; import fs from 'fs';
// eslint-disable-next-line
import electron, {OpenDialogOptions, remote} from 'electron';
import {Atom, DataTableManager, getFlipperLib} from 'flipper-plugin'; import {Atom, DataTableManager, getFlipperLib} from 'flipper-plugin';
import {createContext} from 'react'; import {createContext} from 'react';
import {Header, Request} from '../types'; import {Header, Request} from '../types';