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

View File

@@ -57,7 +57,7 @@ exports[`load PluginInstaller list 1`] = `
tabindex="0"
>
<div
class="css-18abd42-View-FlexBox-FlexColumn ecr18to0"
class="ant-dropdown-trigger css-18abd42-View-FlexBox-FlexColumn ecr18to0"
>
<div
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"
>
<div
class="css-18abd42-View-FlexBox-FlexColumn ecr18to0"
class="ant-dropdown-trigger css-18abd42-View-FlexBox-FlexColumn ecr18to0"
>
<div
class="css-hg3ptm-View-FlexBox-FlexRow-TableBodyRowContainer ehuguum1"
@@ -391,7 +391,7 @@ exports[`load PluginInstaller list with one plugin installed 1`] = `
tabindex="0"
>
<div
class="css-18abd42-View-FlexBox-FlexColumn ecr18to0"
class="ant-dropdown-trigger css-18abd42-View-FlexBox-FlexColumn ecr18to0"
>
<div
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"
>
<div
class="css-18abd42-View-FlexBox-FlexColumn ecr18to0"
class="ant-dropdown-trigger css-18abd42-View-FlexBox-FlexColumn ecr18to0"
>
<div
class="css-hg3ptm-View-FlexBox-FlexRow-TableBodyRowContainer ehuguum1"

View File

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

View File

@@ -148,48 +148,50 @@ export function SandyApp() {
) : null;
return (
<Layout.Bottom>
<Layout.Left>
<Layout.Horizontal>
<LeftRail
toplevelSelection={toplevelSelection}
setToplevelSelection={setToplevelSelection}
/>
<_Sidebar width={250} minWidth={220} maxWidth={800} gutter>
{leftMenuContent && (
<TrackingScope scope={toplevelSelection!}>
{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>
<RootElement>
<Layout.Bottom>
<Layout.Left>
<Layout.Horizontal>
<LeftRail
toplevelSelection={toplevelSelection}
setToplevelSelection={setToplevelSelection}
/>
<_Sidebar width={250} minWidth={220} maxWidth={800} gutter>
{leftMenuContent && (
<TrackingScope scope={toplevelSelection!}>
{leftMenuContent}
</TrackingScope>
)}
</TrackingScope>
) : (
<PluginContainer logger={logger} />
)}
{outOfContentsContainer}
</MainContainer>
</Layout.Left>
<_PortalsManager />
</Layout.Bottom>
</_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>
) : (
<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`,
});
const RootElement = styled.div({
display: 'flex',
height: '100%',
});
RootElement.displayName = 'SandyAppRootElement';
function registerStartupTime(logger: Logger) {
// track time since launch
const [s, ns] = process.hrtime();

View File

@@ -7,19 +7,30 @@
* @format
*/
import {
createElement,
useContext,
useCallback,
forwardRef,
Ref,
ReactElement,
} from 'react';
import {ContextMenuContext} from './ContextMenuProvider';
import * as React from 'react';
import {Menu, Dropdown} from 'antd';
import {createElement, useCallback, forwardRef, Ref, ReactElement} from 'react';
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> = {
/** List of items in the context menu. Used for static menus. */
@@ -33,6 +44,8 @@ type Props<C> = {
onMouseDown?: (e: React.MouseEvent) => any;
} & C;
const contextMenuTrigger = ['contextMenu' as const];
/**
* Native context menu that is shown on secondary click.
* 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>,
ref: Ref<any> | null,
) {
const contextMenuManager = useContext(ContextMenuContext);
const onContextMenu = useCallback(() => {
if (items != null) {
contextMenuManager?.appendToContextMenu(items);
} else if (buildItems != null) {
contextMenuManager?.appendToContextMenu(buildItems());
}
return createContextMenu(items ?? buildItems?.() ?? []);
}, [items, buildItems]);
return createElement(
component || FlexColumn,
{
ref,
onContextMenu,
...otherProps,
},
children,
return (
<Dropdown overlay={onContextMenu} trigger={contextMenuTrigger}>
{createElement(
component || FlexColumn,
{
ref,
...otherProps,
},
children,
)}
</Dropdown>
);
}) 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 Text from '../Text';
import styled from '@emotion/styled';
import electron, {MenuItemConstructorOptions} from 'electron';
import React from 'react';
import {Property} from 'csstype';
import {theme} from 'flipper-plugin';
import {ContextMenuItem, createContextMenu} from '../ContextMenu';
import {Dropdown} from 'antd';
const Token = styled(Text)<{focused?: boolean; color?: Property.Color}>(
(props) => ({
display: 'inline-flex',
alignItems: 'center',
backgroundColor: props.focused
? theme.textColorActive
: props.color || theme.buttonDefaultBackground,
backgroundColor: props.color || theme.buttonDefaultBackground,
borderRadius: 4,
marginRight: 4,
padding: 4,
paddingLeft: 6,
height: 21,
color: props.focused ? 'white' : 'inherit',
'&:active': {
backgroundColor: theme.textColorActive,
color: theme.textColorPrimary,
@@ -103,8 +101,6 @@ type Props = {
};
export default class FilterToken extends PureComponent<Props> {
_ref?: Element | null;
onMouseDown = () => {
if (
this.props.filter.type !== 'enum' ||
@@ -117,7 +113,7 @@ export default class FilterToken extends PureComponent<Props> {
};
showDetails = () => {
const menuTemplate: Array<MenuItemConstructorOptions> = [];
const menuTemplate: Array<ContextMenuItem> = [];
if (this.props.filter.type === 'enum') {
menuTemplate.push(
@@ -155,19 +151,7 @@ export default class FilterToken extends PureComponent<Props> {
},
);
}
const menu = electron.remote.Menu.buildFromTemplate(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),
});
}
return createContextMenu(menuTemplate);
};
toggleFilter = () => {
@@ -202,10 +186,6 @@ export default class FilterToken extends PureComponent<Props> {
}
};
setRef = (ref: HTMLSpanElement | null) => {
this._ref = ref;
};
render() {
const {filter} = this.props;
let color;
@@ -231,21 +211,24 @@ export default class FilterToken extends PureComponent<Props> {
}
return (
<Token
key={`${filter.key}:${value}=${filter.type}`}
tabIndex={-1}
onMouseDown={this.onMouseDown}
focused={this.props.focused}
color={color}
ref={this.setRef}>
<Key type={this.props.filter.type} focused={this.props.focused}>
{filter.key}
</Key>
<Value>{value}</Value>
<Chevron tabIndex={-1} focused={this.props.focused}>
&#8964;
</Chevron>
</Token>
<Dropdown trigger={dropdownTrigger} overlay={this.showDetails}>
<Token
key={`${filter.key}:${value}=${filter.type}`}
tabIndex={-1}
onMouseDown={this.onMouseDown}
focused={this.props.focused}
color={color}>
<Key type={this.props.filter.type} focused={this.props.focused}>
{filter.key}
</Key>
<Value>{value}</Value>
<Chevron tabIndex={-1} focused={this.props.focused}>
&#8964;
</Chevron>
</Token>
</Dropdown>
);
}
}
const dropdownTrigger = ['click' as const];

View File

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

View File

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

View File

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

View File

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