Add plugin actions menu
Summary: See D32311662 for details Reviewed By: mweststrate Differential Revision: D32329804 fbshipit-source-id: 26670353fdf8580643afcb8bc3493384146f5574
This commit is contained in:
committed by
Facebook GitHub Bot
parent
2591d1629e
commit
ed5c2bd39f
@@ -19,11 +19,9 @@ import {
|
|||||||
import {setStaticView} from './reducers/connections';
|
import {setStaticView} from './reducers/connections';
|
||||||
import {Store} from './reducers/';
|
import {Store} from './reducers/';
|
||||||
import electron, {MenuItemConstructorOptions} from 'electron';
|
import electron, {MenuItemConstructorOptions} from 'electron';
|
||||||
import {notNull} from './utils/typeUtils';
|
|
||||||
import constants from './fb-stubs/constants';
|
import constants from './fb-stubs/constants';
|
||||||
import {Logger} from 'flipper-common';
|
import {Logger} from 'flipper-common';
|
||||||
import {
|
import {
|
||||||
NormalizedMenuEntry,
|
|
||||||
_buildInMenuEntries,
|
_buildInMenuEntries,
|
||||||
_wrapInteractionHandler,
|
_wrapInteractionHandler,
|
||||||
getFlipperLib,
|
getFlipperLib,
|
||||||
@@ -46,7 +44,6 @@ export type KeyboardAction = {
|
|||||||
action: string;
|
action: string;
|
||||||
label: string;
|
label: string;
|
||||||
accelerator?: string;
|
accelerator?: string;
|
||||||
topLevelMenu: TopLevelMenu;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type KeyboardActions = Array<DefaultKeyboardAction | KeyboardAction>;
|
export type KeyboardActions = Array<DefaultKeyboardAction | KeyboardAction>;
|
||||||
@@ -73,23 +70,6 @@ export function setupMenuBar(
|
|||||||
store,
|
store,
|
||||||
logger,
|
logger,
|
||||||
);
|
);
|
||||||
// collect all keyboard actions from all plugins
|
|
||||||
const registeredActions = new Set(
|
|
||||||
plugins
|
|
||||||
.map((plugin) => plugin.keyboardActions || [])
|
|
||||||
.flat()
|
|
||||||
.map((action: DefaultKeyboardAction | KeyboardAction) =>
|
|
||||||
typeof action === 'string' ? _buildInMenuEntries[action] : action,
|
|
||||||
)
|
|
||||||
.filter(notNull),
|
|
||||||
);
|
|
||||||
|
|
||||||
// add keyboard actions to
|
|
||||||
registeredActions.forEach((keyboardAction) => {
|
|
||||||
if (keyboardAction != null) {
|
|
||||||
appendMenuItem(template, keyboardAction);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// create actual menu instance
|
// create actual menu instance
|
||||||
const applicationMenu = electron.remote.Menu.buildFromTemplate(template);
|
const applicationMenu = electron.remote.Menu.buildFromTemplate(template);
|
||||||
@@ -98,27 +78,6 @@ export function setupMenuBar(
|
|||||||
electron.remote.Menu.setApplicationMenu(applicationMenu);
|
electron.remote.Menu.setApplicationMenu(applicationMenu);
|
||||||
}
|
}
|
||||||
|
|
||||||
function appendMenuItem(
|
|
||||||
template: Array<MenuItemConstructorOptions>,
|
|
||||||
item: KeyboardAction,
|
|
||||||
) {
|
|
||||||
const keyboardAction = item;
|
|
||||||
if (keyboardAction == null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const itemIndex = template.findIndex(
|
|
||||||
(menu) => menu.label === keyboardAction.topLevelMenu,
|
|
||||||
);
|
|
||||||
if (itemIndex > -1 && template[itemIndex].submenu != null) {
|
|
||||||
(template[itemIndex].submenu as MenuItemConstructorOptions[]).push({
|
|
||||||
click: () => actionHandler(keyboardAction.action),
|
|
||||||
label: keyboardAction.label,
|
|
||||||
accelerator: keyboardAction.accelerator,
|
|
||||||
enabled: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function activateMenuItems(
|
export function activateMenuItems(
|
||||||
activePlugin:
|
activePlugin:
|
||||||
| FlipperPlugin<any, any, any>
|
| FlipperPlugin<any, any, any>
|
||||||
@@ -156,57 +115,6 @@ export function activateMenuItems(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addSandyPluginEntries(entries: NormalizedMenuEntry[]) {
|
|
||||||
if (!electron.remote.Menu) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// disable all keyboard actions
|
|
||||||
for (const item of menuItems) {
|
|
||||||
item[1].enabled = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
pluginActionHandler = (action: string) => {
|
|
||||||
entries.find((e) => e.action === action)?.handler();
|
|
||||||
};
|
|
||||||
|
|
||||||
let changedItems = false;
|
|
||||||
const currentMenu = electron.remote.Menu.getApplicationMenu();
|
|
||||||
for (const entry of entries) {
|
|
||||||
const item = menuItems.get(entry.action!);
|
|
||||||
if (item) {
|
|
||||||
item.enabled = true;
|
|
||||||
item.accelerator = entry.accelerator;
|
|
||||||
} else {
|
|
||||||
const parent = currentMenu?.items.find(
|
|
||||||
(i) => i.label === entry.topLevelMenu,
|
|
||||||
);
|
|
||||||
if (parent) {
|
|
||||||
const item = new electron.remote.MenuItem({
|
|
||||||
enabled: true,
|
|
||||||
click: _wrapInteractionHandler(
|
|
||||||
() => pluginActionHandler?.(entry.action!),
|
|
||||||
'MenuItem',
|
|
||||||
'onClick',
|
|
||||||
'flipper:menu:' + entry.topLevelMenu,
|
|
||||||
entry.label,
|
|
||||||
),
|
|
||||||
label: entry.label,
|
|
||||||
accelerator: entry.accelerator,
|
|
||||||
});
|
|
||||||
parent.submenu!.append(item);
|
|
||||||
menuItems.set(entry.action!, item);
|
|
||||||
changedItems = true;
|
|
||||||
} else {
|
|
||||||
console.warn('Invalid top level menu: ' + entry.topLevelMenu);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (changedItems) {
|
|
||||||
electron.remote.Menu.setApplicationMenu(currentMenu);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function trackMenuItems(menu: string, items: MenuItemConstructorOptions[]) {
|
function trackMenuItems(menu: string, items: MenuItemConstructorOptions[]) {
|
||||||
items.forEach((item) => {
|
items.forEach((item) => {
|
||||||
if (item.label && item.click) {
|
if (item.label && item.click) {
|
||||||
|
|||||||
@@ -59,7 +59,11 @@ export interface RenderHost {
|
|||||||
showSaveDialog?: FlipperLib['showSaveDialog'];
|
showSaveDialog?: FlipperLib['showSaveDialog'];
|
||||||
showOpenDialog?: FlipperLib['showOpenDialog'];
|
showOpenDialog?: FlipperLib['showOpenDialog'];
|
||||||
showSelectDirectoryDialog?(defaultPath?: string): Promise<string | undefined>;
|
showSelectDirectoryDialog?(defaultPath?: string): Promise<string | undefined>;
|
||||||
registerShortcut(shortCut: string, callback: () => void): void;
|
/**
|
||||||
|
* @returns
|
||||||
|
* A callback to unregister the shortcut
|
||||||
|
*/
|
||||||
|
registerShortcut(shortCut: string, callback: () => void): () => void;
|
||||||
hasFocus(): boolean;
|
hasFocus(): boolean;
|
||||||
onIpcEvent<Event extends keyof MainProcessEvents>(
|
onIpcEvent<Event extends keyof MainProcessEvents>(
|
||||||
event: Event,
|
event: Event,
|
||||||
@@ -96,7 +100,9 @@ if (process.env.NODE_ENV === 'test') {
|
|||||||
return '';
|
return '';
|
||||||
},
|
},
|
||||||
writeTextToClipboard() {},
|
writeTextToClipboard() {},
|
||||||
registerShortcut() {},
|
registerShortcut() {
|
||||||
|
return () => undefined;
|
||||||
|
},
|
||||||
hasFocus() {
|
hasFocus() {
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ Object {
|
|||||||
"off": [MockFunction],
|
"off": [MockFunction],
|
||||||
"on": [MockFunction],
|
"on": [MockFunction],
|
||||||
},
|
},
|
||||||
|
"pluginMenuEntries": Array [],
|
||||||
"selectedAppId": "TestApp#Android#MockAndroidDevice#serial",
|
"selectedAppId": "TestApp#Android#MockAndroidDevice#serial",
|
||||||
"selectedAppPluginListRevision": 0,
|
"selectedAppPluginListRevision": 0,
|
||||||
"selectedDevice": Object {
|
"selectedDevice": Object {
|
||||||
|
|||||||
144
desktop/app/src/chrome/PluginActionsMenu.tsx
Normal file
144
desktop/app/src/chrome/PluginActionsMenu.tsx
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
/**
|
||||||
|
* 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 Icon from '@ant-design/icons';
|
||||||
|
import {css} from '@emotion/css';
|
||||||
|
import {Button, Menu, MenuItemProps} from 'antd';
|
||||||
|
import {
|
||||||
|
NormalizedMenuEntry,
|
||||||
|
TrackingScope,
|
||||||
|
useTrackedCallback,
|
||||||
|
} from 'flipper-plugin';
|
||||||
|
import React, {useEffect} from 'react';
|
||||||
|
import {getRenderHostInstance} from '../RenderHost';
|
||||||
|
import {getActivePlugin} from '../selectors/connections';
|
||||||
|
import {useStore} from '../utils/useStore';
|
||||||
|
|
||||||
|
function MagicIcon() {
|
||||||
|
return (
|
||||||
|
// https://www.svgrepo.com/svg/59702/magic
|
||||||
|
<svg
|
||||||
|
width="1em"
|
||||||
|
height="1em"
|
||||||
|
viewBox="0 0 464.731 464.731"
|
||||||
|
fill="currentColor">
|
||||||
|
<title>Magic</title>
|
||||||
|
<path
|
||||||
|
d="M463.056,441.971l-45.894-43.145l29.759-55.521c0.8-1.508,0.379-3.398-1.029-4.395
|
||||||
|
c-1.388-1.011-3.305-0.832-4.487,0.424l-43.146,45.895l-55.533-29.746c-1.515-0.803-3.399-0.377-4.395,1.027
|
||||||
|
c-1.017,1.392-0.815,3.309,0.438,4.488l45.911,43.162l-29.747,55.518c-0.816,1.525-0.378,3.401,1.01,4.412
|
||||||
|
c1.41,0.996,3.326,0.816,4.502-0.438l43.149-45.912l55.507,29.746c1.506,0.802,3.393,0.378,4.393-1.027
|
||||||
|
C464.506,445.072,464.308,443.136,463.056,441.971z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M369.086,94.641l-20.273,37.826c-1.04,1.918-0.479,4.307,1.285,5.588c1.783,1.271,4.215,1.029,5.71-0.559
|
||||||
|
l29.417-31.269l37.78,20.26c1.921,1.024,4.323,0.484,5.589-1.285c1.271-1.783,1.048-4.215-0.555-5.709l-31.245-29.385
|
||||||
|
l20.274-37.814c1.028-1.918,0.466-4.307-1.297-5.59c-1.766-1.268-4.216-1.025-5.713,0.558l-29.381,31.257l-37.814-20.273
|
||||||
|
c-1.936-1.026-4.325-0.467-5.589,1.301c-1.273,1.766-1.042,4.214,0.544,5.711L369.086,94.641z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M123.956,360.06l-44.659,6.239l-17.611-41.484c-0.906-2.113-3.217-3.232-5.423-2.631
|
||||||
|
c-2.226,0.623-3.626,2.78-3.313,5.051l6.239,44.639L17.69,389.489c-2.1,0.908-3.23,3.217-2.614,5.424
|
||||||
|
c0.609,2.219,2.767,3.629,5.032,3.31l44.657-6.241l17.611,41.5c0.896,2.118,3.218,3.236,5.425,2.629
|
||||||
|
c2.206-0.617,3.626-2.765,3.312-5.043l-6.238-44.658l41.5-17.617c2.099-0.904,3.234-3.217,2.612-5.423
|
||||||
|
C128.383,361.147,126.221,359.745,123.956,360.06z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M4.908,45.161l34.646,9.537l-0.23,35.832c-0.012,2.01,1.449,3.704,3.447,3.99
|
||||||
|
c1.976,0.271,3.851-0.969,4.377-2.901l9.521-34.565l35.923,0.225c2.01,0.016,3.702-1.447,3.992-3.441
|
||||||
|
c0.271-1.982-0.97-3.853-2.905-4.383l-34.627-9.547l0.213-35.881c0.018-2.01-1.466-3.701-3.441-3.988
|
||||||
|
c-1.983-0.273-3.856,0.965-4.383,2.901l-9.533,34.608L5.996,37.324c-1.991,0-3.701,1.463-3.974,3.441
|
||||||
|
C1.751,42.747,2.992,44.633,4.908,45.161z"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M278.019,234.519l139.775-18.477c1.586-0.21,2.762-1.555,2.762-3.143c0-1.587-1.176-2.928-2.762-3.142
|
||||||
|
L278.019,191.28l20.476-57.755c0.857-2.446,0.235-5.183-1.603-7.009c-1.828-1.844-4.567-2.445-7.01-1.586l-57.697,20.484
|
||||||
|
L213.708,5.688c-0.194-1.588-1.554-2.764-3.14-2.764c-1.584,0-2.935,1.176-3.146,2.764l-18.457,139.744l-57.772-20.502
|
||||||
|
c-2.448-0.875-5.181-0.258-7.014,1.586c-1.84,1.826-2.46,4.563-1.586,7.009l20.489,57.772l-139.73,18.46
|
||||||
|
c-1.584,0.214-2.762,1.555-2.762,3.142c0,1.588,1.178,2.933,2.762,3.143l139.73,18.461l-20.489,57.742
|
||||||
|
c-0.874,2.447-0.254,5.182,1.586,7.01c1.833,1.842,4.565,2.462,7.014,1.582l57.772-20.467l18.457,139.743
|
||||||
|
c0.212,1.583,1.563,2.764,3.146,2.764c1.586,0,2.945-1.181,3.14-2.764l18.477-139.743l57.727,20.486
|
||||||
|
c2.441,0.876,5.181,0.256,7.009-1.589c1.845-1.825,2.461-4.562,1.584-7.007L278.019,234.519z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const menu = css`
|
||||||
|
border: none;
|
||||||
|
`;
|
||||||
|
const submenu = css`
|
||||||
|
.ant-menu-submenu-title {
|
||||||
|
width: 32px;
|
||||||
|
height: 32px !important;
|
||||||
|
line-height: 32px !important;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
.ant-menu-submenu-arrow {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
function PluginActionMenuItem({
|
||||||
|
label,
|
||||||
|
action,
|
||||||
|
handler,
|
||||||
|
accelerator,
|
||||||
|
// Some props like `eventKey` are auto-generated by ant-design
|
||||||
|
// We need to pass them through to MenuItem
|
||||||
|
...antdProps
|
||||||
|
}: NormalizedMenuEntry & MenuItemProps) {
|
||||||
|
const trackedHandler = useTrackedCallback(action, handler, [action, handler]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (accelerator) {
|
||||||
|
const unregister = getRenderHostInstance().registerShortcut(
|
||||||
|
accelerator,
|
||||||
|
trackedHandler,
|
||||||
|
);
|
||||||
|
return unregister;
|
||||||
|
}
|
||||||
|
}, [trackedHandler, accelerator]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu.Item onClick={trackedHandler} {...antdProps}>
|
||||||
|
{label}
|
||||||
|
</Menu.Item>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
export function PluginActionsMenu() {
|
||||||
|
const menuEntries = useStore((state) => state.connections.pluginMenuEntries);
|
||||||
|
const activePlugin = useStore(getActivePlugin);
|
||||||
|
|
||||||
|
if (!menuEntries.length || !activePlugin) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<TrackingScope scope={`PluginActionsButton:${activePlugin.details.id}`}>
|
||||||
|
<Menu mode="vertical" className={menu} selectable={false}>
|
||||||
|
<Menu.SubMenu
|
||||||
|
popupOffset={[15, 0]}
|
||||||
|
key="pluginActions"
|
||||||
|
title={
|
||||||
|
<Button
|
||||||
|
icon={<Icon component={MagicIcon} />}
|
||||||
|
title="Plugin actions"
|
||||||
|
type="ghost"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
className={submenu}>
|
||||||
|
{menuEntries.map((entry) => (
|
||||||
|
<PluginActionMenuItem key={entry.action} {...entry} />
|
||||||
|
))}
|
||||||
|
</Menu.SubMenu>
|
||||||
|
</Menu>
|
||||||
|
</TrackingScope>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -71,6 +71,7 @@ export function initializeElectron() {
|
|||||||
},
|
},
|
||||||
registerShortcut(shortcut, callback) {
|
registerShortcut(shortcut, callback) {
|
||||||
remote.globalShortcut.register(shortcut, callback);
|
remote.globalShortcut.register(shortcut, callback);
|
||||||
|
return () => remote.globalShortcut.unregister(shortcut);
|
||||||
},
|
},
|
||||||
hasFocus() {
|
hasFocus() {
|
||||||
return remote.getCurrentWindow().isFocused();
|
return remote.getCurrentWindow().isFocused();
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {getPluginKey} from '../utils/pluginKey';
|
|||||||
import {deconstructClientId} from 'flipper-common';
|
import {deconstructClientId} from 'flipper-common';
|
||||||
import type {RegisterPluginAction} from './plugins';
|
import type {RegisterPluginAction} from './plugins';
|
||||||
import {shallowEqual} from 'react-redux';
|
import {shallowEqual} from 'react-redux';
|
||||||
|
import {NormalizedMenuEntry} from 'flipper-plugin';
|
||||||
|
|
||||||
export type StaticViewProps = {logger: Logger};
|
export type StaticViewProps = {logger: Logger};
|
||||||
|
|
||||||
@@ -68,6 +69,7 @@ type StateV2 = {
|
|||||||
selectedDevice: null | BaseDevice;
|
selectedDevice: null | BaseDevice;
|
||||||
selectedPlugin: null | string;
|
selectedPlugin: null | string;
|
||||||
selectedAppId: null | string; // Full quantified identifier of the app
|
selectedAppId: null | string; // Full quantified identifier of the app
|
||||||
|
pluginMenuEntries: NormalizedMenuEntry[];
|
||||||
userPreferredDevice: null | string;
|
userPreferredDevice: null | string;
|
||||||
userPreferredPlugin: null | string;
|
userPreferredPlugin: null | string;
|
||||||
userPreferredApp: null | string; // The name of the preferred app, e.g. Facebook
|
userPreferredApp: null | string; // The name of the preferred app, e.g. Facebook
|
||||||
@@ -110,6 +112,10 @@ export type Action =
|
|||||||
time: number;
|
time: number;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
| {
|
||||||
|
type: 'SET_MENU_ENTRIES';
|
||||||
|
payload: NormalizedMenuEntry[];
|
||||||
|
}
|
||||||
| {
|
| {
|
||||||
type: 'NEW_CLIENT';
|
type: 'NEW_CLIENT';
|
||||||
payload: Client;
|
payload: Client;
|
||||||
@@ -173,6 +179,7 @@ const INITAL_STATE: State = {
|
|||||||
selectedDevice: null,
|
selectedDevice: null,
|
||||||
selectedAppId: null,
|
selectedAppId: null,
|
||||||
selectedPlugin: DEFAULT_PLUGIN,
|
selectedPlugin: DEFAULT_PLUGIN,
|
||||||
|
pluginMenuEntries: [],
|
||||||
userPreferredDevice: null,
|
userPreferredDevice: null,
|
||||||
userPreferredPlugin: null,
|
userPreferredPlugin: null,
|
||||||
userPreferredApp: null,
|
userPreferredApp: null,
|
||||||
@@ -230,6 +237,10 @@ export default (state: State = INITAL_STATE, action: Actions): State => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case 'SET_MENU_ENTRIES': {
|
||||||
|
return {...state, pluginMenuEntries: action.payload};
|
||||||
|
}
|
||||||
|
|
||||||
case 'REGISTER_DEVICE': {
|
case 'REGISTER_DEVICE': {
|
||||||
const {payload} = action;
|
const {payload} = action;
|
||||||
|
|
||||||
@@ -479,6 +490,11 @@ export const selectPlugin = (payload: {
|
|||||||
payload: {...payload, time: payload.time ?? Date.now()},
|
payload: {...payload, time: payload.time ?? Date.now()},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const setMenuEntries = (menuEntries: NormalizedMenuEntry[]): Action => ({
|
||||||
|
type: 'SET_MENU_ENTRIES',
|
||||||
|
payload: menuEntries,
|
||||||
|
});
|
||||||
|
|
||||||
export const selectClient = (clientId: string): Action => ({
|
export const selectClient = (clientId: string): Action => ({
|
||||||
type: 'SELECT_CLIENT',
|
type: 'SELECT_CLIENT',
|
||||||
payload: clientId,
|
payload: clientId,
|
||||||
|
|||||||
@@ -188,9 +188,9 @@ export const LeftRail = withTrackingScope(function LeftRail({
|
|||||||
<WelcomeScreenButton />
|
<WelcomeScreenButton />
|
||||||
<ShowSettingsButton />
|
<ShowSettingsButton />
|
||||||
<SupportFormButton />
|
<SupportFormButton />
|
||||||
|
<ImportExportButton />
|
||||||
<RightSidebarToggleButton />
|
<RightSidebarToggleButton />
|
||||||
<LeftSidebarToggleButton />
|
<LeftSidebarToggleButton />
|
||||||
<ImportExportButton />
|
|
||||||
{config.showLogin && <LoginButton />}
|
{config.showLogin && <LoginButton />}
|
||||||
</Layout.Container>
|
</Layout.Container>
|
||||||
</Layout.Bottom>
|
</Layout.Bottom>
|
||||||
@@ -233,7 +233,7 @@ function ImportExportButton() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu mode="vertical" className={menu}>
|
<Menu mode="vertical" className={menu} selectable={false}>
|
||||||
<SubMenu
|
<SubMenu
|
||||||
popupOffset={[10, 0]}
|
popupOffset={[10, 0]}
|
||||||
key="importexport"
|
key="importexport"
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import {
|
|||||||
getMetroDevice,
|
getMetroDevice,
|
||||||
} from '../../selectors/connections';
|
} from '../../selectors/connections';
|
||||||
import * as connections from '../../selectors/connections';
|
import * as connections from '../../selectors/connections';
|
||||||
|
import {PluginActionsMenu} from '../../chrome/PluginActionsMenu';
|
||||||
|
|
||||||
const {Text} = Typography;
|
const {Text} = Typography;
|
||||||
|
|
||||||
@@ -70,6 +71,8 @@ export function AppInspect() {
|
|||||||
<Toolbar gap>
|
<Toolbar gap>
|
||||||
<MetroButton />
|
<MetroButton />
|
||||||
<ScreenCaptureButtons />
|
<ScreenCaptureButtons />
|
||||||
|
<div style={{flex: 1}} />
|
||||||
|
<PluginActionsMenu />
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
)}
|
)}
|
||||||
</Layout.Container>
|
</Layout.Container>
|
||||||
|
|||||||
@@ -115,11 +115,10 @@ export function createSandyPluginWrapper<S, A extends BaseAction, P>(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const {action, label, accelerator, topLevelMenu} = def;
|
const {action, label, accelerator} = def;
|
||||||
return {
|
return {
|
||||||
label,
|
label,
|
||||||
accelerator,
|
accelerator,
|
||||||
topLevelMenu,
|
|
||||||
handler() {
|
handler() {
|
||||||
executeKeyboardAction(action);
|
executeKeyboardAction(action);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,19 +18,18 @@ import {addNotification} from '../reducers/notifications';
|
|||||||
import {deconstructPluginKey} from 'flipper-common';
|
import {deconstructPluginKey} from 'flipper-common';
|
||||||
import {DetailSidebarImpl} from '../sandy-chrome/DetailSidebarImpl';
|
import {DetailSidebarImpl} from '../sandy-chrome/DetailSidebarImpl';
|
||||||
import {RenderHost} from '../RenderHost';
|
import {RenderHost} from '../RenderHost';
|
||||||
|
import {setMenuEntries} from '../reducers/connections';
|
||||||
|
|
||||||
export function initializeFlipperLibImplementation(
|
export function initializeFlipperLibImplementation(
|
||||||
renderHost: RenderHost,
|
renderHost: RenderHost,
|
||||||
store: Store,
|
store: Store,
|
||||||
logger: Logger,
|
logger: Logger,
|
||||||
) {
|
) {
|
||||||
// late require to avoid cyclic dependency
|
|
||||||
const {addSandyPluginEntries} = require('../MenuBar');
|
|
||||||
_setFlipperLibImplementation({
|
_setFlipperLibImplementation({
|
||||||
isFB: !constants.IS_PUBLIC_BUILD,
|
isFB: !constants.IS_PUBLIC_BUILD,
|
||||||
logger,
|
logger,
|
||||||
enableMenuEntries(entries) {
|
enableMenuEntries(entries) {
|
||||||
addSandyPluginEntries(entries);
|
store.dispatch(setMenuEntries(entries));
|
||||||
},
|
},
|
||||||
createPaste,
|
createPaste,
|
||||||
GK(gatekeeper: string) {
|
GK(gatekeeper: string) {
|
||||||
|
|||||||
@@ -467,7 +467,6 @@ test('plugins can register menu entries', async () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Custom Action',
|
label: 'Custom Action',
|
||||||
topLevelMenu: 'Edit',
|
|
||||||
handler() {
|
handler() {
|
||||||
counter.set(counter.get() + 3);
|
counter.set(counter.get() + 3);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,15 +7,12 @@
|
|||||||
* @format
|
* @format
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export type TopLevelMenu = 'Edit' | 'View' | 'Window' | 'Help';
|
|
||||||
|
|
||||||
export type MenuEntry = BuiltInMenuEntry | CustomMenuEntry;
|
export type MenuEntry = BuiltInMenuEntry | CustomMenuEntry;
|
||||||
export type DefaultKeyboardAction = keyof typeof buildInMenuEntries;
|
export type DefaultKeyboardAction = keyof typeof buildInMenuEntries;
|
||||||
|
|
||||||
export type NormalizedMenuEntry = {
|
export type NormalizedMenuEntry = {
|
||||||
label: string;
|
label: string;
|
||||||
accelerator?: string;
|
accelerator?: string;
|
||||||
topLevelMenu: TopLevelMenu;
|
|
||||||
handler: () => void;
|
handler: () => void;
|
||||||
action: string;
|
action: string;
|
||||||
};
|
};
|
||||||
@@ -23,7 +20,6 @@ export type NormalizedMenuEntry = {
|
|||||||
export type CustomMenuEntry = {
|
export type CustomMenuEntry = {
|
||||||
label: string;
|
label: string;
|
||||||
accelerator?: string;
|
accelerator?: string;
|
||||||
topLevelMenu: TopLevelMenu;
|
|
||||||
handler: () => void;
|
handler: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -36,18 +32,15 @@ export const buildInMenuEntries = {
|
|||||||
clear: {
|
clear: {
|
||||||
label: 'Clear',
|
label: 'Clear',
|
||||||
accelerator: 'CmdOrCtrl+K',
|
accelerator: 'CmdOrCtrl+K',
|
||||||
topLevelMenu: 'View',
|
|
||||||
action: 'clear',
|
action: 'clear',
|
||||||
},
|
},
|
||||||
goToBottom: {
|
goToBottom: {
|
||||||
label: 'Go To Bottom',
|
label: 'Go To Bottom',
|
||||||
accelerator: 'CmdOrCtrl+B',
|
accelerator: 'CmdOrCtrl+B',
|
||||||
topLevelMenu: 'View',
|
|
||||||
action: 'goToBottom',
|
action: 'goToBottom',
|
||||||
},
|
},
|
||||||
createPaste: {
|
createPaste: {
|
||||||
label: 'Create Paste',
|
label: 'Create Paste',
|
||||||
topLevelMenu: 'Edit',
|
|
||||||
action: 'createPaste',
|
action: 'createPaste',
|
||||||
},
|
},
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
@@ -38,7 +38,6 @@ export function plugin(client: PluginClient<Events, {}>) {
|
|||||||
client.addMenuEntry(
|
client.addMenuEntry(
|
||||||
{
|
{
|
||||||
label: 'Reset Selection',
|
label: 'Reset Selection',
|
||||||
topLevelMenu: 'Edit',
|
|
||||||
handler: () => {
|
handler: () => {
|
||||||
selectedID.set(null);
|
selectedID.set(null);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -250,7 +250,6 @@ Example:
|
|||||||
```typescript
|
```typescript
|
||||||
client.addMenuEntry({
|
client.addMenuEntry({
|
||||||
label: 'Reset Selection',
|
label: 'Reset Selection',
|
||||||
topLevelMenu: 'Edit',
|
|
||||||
accelerator: 'CmdOrCtrl+R'
|
accelerator: 'CmdOrCtrl+R'
|
||||||
handler: () => {
|
handler: () => {
|
||||||
// Event handling
|
// Event handling
|
||||||
@@ -258,9 +257,9 @@ client.addMenuEntry({
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
The `accelerator` argument is optional, but describes the keyboard shortcut. See the [Electron docs](https://www.electronjs.org/docs/api/accelerator) for their format. The `topLevelMenu` must be one of `"Edit"`, `"View"`, `"Window"` or `"Help"`.
|
The `accelerator` argument is optional, but describes the keyboard shortcut. See the [Electron docs](https://www.electronjs.org/docs/api/accelerator) for their format.
|
||||||
|
|
||||||
It is possible to leave out the `label`, `topLevelMenu` and `accelerator` fields if a pre-defined `action` is set, which configures all three of them.
|
It is possible to leave out the `label`, and `accelerator` fields if a pre-defined `action` is set, which configures all three of them.
|
||||||
The currently pre-defined actions are `"Clear"`, `"Go To Bottom"` and `"Create Paste"`.
|
The currently pre-defined actions are `"Clear"`, `"Go To Bottom"` and `"Create Paste"`.
|
||||||
Example of using a pre-defined action:
|
Example of using a pre-defined action:
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user