Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: control Flow Editor via the Electron app menu #3660

Merged
merged 20 commits into from
Jul 31, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions Composer/packages/client/src/hooks/useElectronFeatures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { useEffect } from 'react';
import get from 'lodash/get';
import { getEditorAPI } from '@bfc/shared';

export const useElectronFeatures = (actionSelected: boolean) => {
// Sync selection state to Electron main process
useEffect(() => {
if (!window.__IS_ELECTRON__) return;
if (!window.ipcRenderer || typeof window.ipcRenderer.send !== 'function') return;

window.ipcRenderer.send('composer-state-change', { actionSelected });
}, [actionSelected]);

// Subscribe Electron app menu events (copy/cut/del/undo/redo)
useEffect(() => {
if (!window.__IS_ELECTRON__) return;
if (!window.ipcRenderer || typeof window.ipcRenderer.on !== 'function') return;

const EditorAPI = getEditorAPI();
window.ipcRenderer.on('electron-menu-clicked', (e, data) => {
const label = get(data, 'label', '');
switch (label) {
case 'undo':
return EditorAPI.Editing.Undo();
case 'redo':
return EditorAPI.Editing.Redo();
case 'cut':
return EditorAPI.Actions.CutSelection();
case 'copy':
return EditorAPI.Actions.CopySelection();
case 'delete':
return EditorAPI.Actions.DeleteSelection();
}
});
}, []);
};
23 changes: 14 additions & 9 deletions Composer/packages/client/src/pages/design/DesignPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Breadcrumb, IBreadcrumbItem } from 'office-ui-fabric-react/lib/Breadcru
import formatMessage from 'format-message';
import { globalHistory, RouteComponentProps } from '@reach/router';
import get from 'lodash/get';
import { DialogFactory, SDKKinds, DialogInfo, PromptTab, LuIntentSection } from '@bfc/shared';
import { DialogFactory, SDKKinds, DialogInfo, PromptTab, LuIntentSection, getEditorAPI } from '@bfc/shared';
import { ActionButton } from 'office-ui-fabric-react/lib/Button';
import { JsonEditor } from '@bfc/code-editor';
import { useTriggerApi } from '@bfc/extension';
Expand Down Expand Up @@ -45,8 +45,8 @@ import {
luFilesState,
localeState,
} from '../../recoilModel';
import { useElectronFeatures } from '../../hooks/useElectronFeatures';

import { VisualEditorAPI } from './FrameAPI';
import {
breadcrumbClass,
contentWrapper,
Expand Down Expand Up @@ -227,14 +227,17 @@ const DesignPage: React.FC<RouteComponentProps<{ dialogId: string; projectId: st
const { actionSelected, showDisableBtn, showEnableBtn } = useMemo(() => {
const actionSelected = Array.isArray(visualEditorSelection) && visualEditorSelection.length > 0;
if (!actionSelected) {
return {};
return { actionSelected: false, showDisableBtn: false, showEnableBtn: false };
}
const selectedActions = visualEditorSelection.map((id) => get(currentDialog?.content, id));
const showDisableBtn = selectedActions.some((x) => get(x, 'disabled') !== true);
const showEnableBtn = selectedActions.some((x) => get(x, 'disabled') === true);
return { actionSelected, showDisableBtn, showEnableBtn };
}, [visualEditorSelection]);

useElectronFeatures(actionSelected);

const EditorAPI = getEditorAPI();
const toolbarItems: IToolbarItem[] = [
{
type: 'dropdown',
Expand Down Expand Up @@ -282,6 +285,7 @@ const DesignPage: React.FC<RouteComponentProps<{ dialogId: string; projectId: st
text: formatMessage('Undo'),
disabled: !undoHistory.canUndo(),
onClick: () => {
// TODO: register EditorAPI.Editing.Undo()
//ToDo undo
},
},
Expand All @@ -290,6 +294,7 @@ const DesignPage: React.FC<RouteComponentProps<{ dialogId: string; projectId: st
text: formatMessage('Redo'),
disabled: !undoHistory.canRedo(),
onClick: () => {
// TODO: register EditorAPI.Editing.Redo()
//ToDo redo
},
},
Expand All @@ -298,31 +303,31 @@ const DesignPage: React.FC<RouteComponentProps<{ dialogId: string; projectId: st
text: formatMessage('Cut'),
disabled: !actionSelected,
onClick: () => {
VisualEditorAPI.cutSelection();
EditorAPI.Actions.CutSelection();
},
},
{
key: 'edit.copy',
text: formatMessage('Copy'),
disabled: !actionSelected,
onClick: () => {
VisualEditorAPI.copySelection();
EditorAPI.Actions.CopySelection();
},
},
{
key: 'edit.move',
text: formatMessage('Move'),
disabled: !actionSelected,
onClick: () => {
VisualEditorAPI.moveSelection();
EditorAPI.Actions.MoveSelection();
},
},
{
key: 'edit.delete',
text: formatMessage('Delete'),
disabled: !actionSelected,
onClick: () => {
VisualEditorAPI.deleteSelection();
EditorAPI.Actions.DeleteSelection();
},
},
],
Expand All @@ -343,15 +348,15 @@ const DesignPage: React.FC<RouteComponentProps<{ dialogId: string; projectId: st
text: formatMessage('Disable'),
disabled: !showDisableBtn,
onClick: () => {
VisualEditorAPI.disableSelection();
EditorAPI.Actions.DisableSelection();
},
},
{
key: 'enable',
text: formatMessage('Enable'),
disabled: !showEnableBtn,
onClick: () => {
VisualEditorAPI.enableSelection();
EditorAPI.Actions.EnableSelection();
},
},
],
Expand Down
31 changes: 0 additions & 31 deletions Composer/packages/client/src/pages/design/FrameAPI.ts

This file was deleted.

4 changes: 2 additions & 2 deletions Composer/packages/electron-server/__tests__/appMenu.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ describe('App menu', () => {

// Edit
expect(menuTemplate[1].label).toBe('Edit');
expect(menuTemplate[1].submenu.length).toBe(9);
expect(menuTemplate[1].submenu.length).toBe(8);

// View
expect(menuTemplate[2].label).toBe('View');
Expand Down Expand Up @@ -75,7 +75,7 @@ describe('App menu', () => {

// Edit
expect(menuTemplate[2].label).toBe('Edit');
expect(menuTemplate[2].submenu.length).toBe(9);
expect(menuTemplate[2].submenu.length).toBe(8);

// View
expect(menuTemplate[3].label).toBe('View');
Expand Down
48 changes: 39 additions & 9 deletions Composer/packages/electron-server/src/appMenu.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { app, dialog, Menu, MenuItemConstructorOptions, shell } from 'electron';
import { app, dialog, Menu, MenuItemConstructorOptions, shell, ipcMain } from 'electron';

import { isMac } from './utility/platform';
import { AppUpdater } from './appUpdater';
Expand Down Expand Up @@ -29,15 +29,14 @@ function getAppMenu(): MenuItemConstructorOptions[] {
function getRestOfEditMenu(): MenuItemConstructorOptions[] {
if (isMac()) {
return [
{ role: 'delete' },
{ type: 'separator' },
{
label: 'Speech',
submenu: [{ role: 'startSpeaking' }, { role: 'stopSpeaking' }],
},
];
}
return [{ role: 'delete' }, { type: 'separator' }, { role: 'selectAll' }];
return [{ type: 'separator' }, { role: 'selectAll' }];
}

function getRestOfWindowMenu(): MenuItemConstructorOptions[] {
Expand All @@ -47,7 +46,14 @@ function getRestOfWindowMenu(): MenuItemConstructorOptions[] {
return [{ role: 'close' }];
}

export function initAppMenu() {
export function initAppMenu(win?: Electron.BrowserWindow) {
// delegate menu events to Renderer process (Composer web app)
const handleMenuEvents = (menuEventName: string) => {
if (win) {
win.webContents.send('electron-menu-clicked', { label: menuEventName });
}
};

const template: MenuItemConstructorOptions[] = [
// App (Mac)
...getAppMenu(),
Expand All @@ -60,12 +66,25 @@ export function initAppMenu() {
{
label: 'Edit',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
// NOTE: Avoid using builtin `role`, it won't override the click handler.
{ id: 'Undo', label: 'Undo', accelerator: 'CmdOrCtrl+Z', click: () => handleMenuEvents('undo') },
{ id: 'Redo', label: 'Redo', accelerator: 'CmdOrCtrl+Shift+Z', click: () => handleMenuEvents('redo') },
{ type: 'separator' },
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
{ id: 'Cut', label: 'Cut', enabled: false, accelerator: 'CmdOrCtrl+X', click: () => handleMenuEvents('cut') },
{
id: 'Copy',
label: 'Copy',
enabled: false,
accelerator: 'CmdOrCtrl+C',
click: () => handleMenuEvents('copy'),
},
{
id: 'Delete',
label: 'Delete',
enabled: false,
accelerator: 'Delete',
click: () => handleMenuEvents('delete'),
},
...getRestOfEditMenu(),
],
},
Expand Down Expand Up @@ -161,4 +180,15 @@ export function initAppMenu() {

const menu = Menu.buildFromTemplate(template);
Menu.setApplicationMenu(menu);

// Let menu enable/disable status reflect action selection states.
ipcMain &&
ipcMain.on &&
ipcMain.on('composer-state-change', (e, state) => {
const actionSelected = !!state.actionSelected;
['Cut', 'Copy', 'Delete'].forEach((id) => {
menu.getMenuItemById(id).enabled = actionSelected;
});
Menu.setApplicationMenu(menu);
});
}
2 changes: 1 addition & 1 deletion Composer/packages/electron-server/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ async function loadServer() {

async function main() {
log('Rendering application...');
initAppMenu();
const mainWindow = ElectronWindow.getInstance().browserWindow;
initAppMenu(mainWindow);
if (mainWindow) {
if (process.env.COMPOSER_DEV_TOOLS) {
mainWindow.webContents.openDevTools();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { DialogUtils, SDKKinds, ShellApi } from '@bfc/shared';
import { DialogUtils, SDKKinds, ShellApi, registerEditorAPI } from '@bfc/shared';
import get from 'lodash/get';
import { useDialogEditApi, useDialogApi, useActionApi } from '@bfc/extension';

Expand Down Expand Up @@ -311,13 +311,14 @@ export const useEditorEventApi = (
return handler(eventData);
};

// HACK: use global handler before we solve iframe state sync problem
(window as any).copySelection = () => handleEditorEvent(NodeEventTypes.CopySelection);
(window as any).cutSelection = () => handleEditorEvent(NodeEventTypes.CutSelection);
(window as any).moveSelection = () => handleEditorEvent(NodeEventTypes.MoveSelection);
(window as any).deleteSelection = () => handleEditorEvent(NodeEventTypes.DeleteSelection);
(window as any).disableSelection = () => handleEditorEvent(NodeEventTypes.DisableSelection);
(window as any).enableSelection = () => handleEditorEvent(NodeEventTypes.EnableSelection);
registerEditorAPI('Actions', {
CopySelection: () => handleEditorEvent(NodeEventTypes.CopySelection),
CutSelection: () => handleEditorEvent(NodeEventTypes.CutSelection),
MoveSelection: () => handleEditorEvent(NodeEventTypes.MoveSelection),
DeleteSelection: () => handleEditorEvent(NodeEventTypes.DeleteSelection),
DisableSelection: () => handleEditorEvent(NodeEventTypes.DisableSelection),
EnableSelection: () => handleEditorEvent(NodeEventTypes.EnableSelection),
});

return {
handleEditorEvent,
Expand Down
66 changes: 66 additions & 0 deletions Composer/packages/lib/shared/src/types/EditorAPI.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

type EditorAPIHandler = () => any;

export interface EditorAPI {
Editing: {
Undo: EditorAPIHandler;
Redo: EditorAPIHandler;
};
Actions: {
CopySelection: EditorAPIHandler;
CutSelection: EditorAPIHandler;
MoveSelection: EditorAPIHandler;
DeleteSelection: EditorAPIHandler;
DisableSelection: EditorAPIHandler;
EnableSelection: EditorAPIHandler;
};
}

const EmptyHandler = () => null;

const DefaultEditorAPI: EditorAPI = {
Editing: {
Undo: EmptyHandler,
Redo: EmptyHandler,
},
Actions: {
CopySelection: EmptyHandler,
CutSelection: EmptyHandler,
MoveSelection: EmptyHandler,
DeleteSelection: EmptyHandler,
DisableSelection: EmptyHandler,
EnableSelection: EmptyHandler,
},
};

const EDITOR_API_NAME = 'EditorAPI';

export function getEditorAPI(): EditorAPI {
if (!window[EDITOR_API_NAME]) {
window[EDITOR_API_NAME] = { ...DefaultEditorAPI };
}
return window[EDITOR_API_NAME];
}

export function registerEditorAPI(domain: 'Editing' | 'Actions', handlers: { [fn: string]: EditorAPIHandler }) {
const editorAPI: EditorAPI = getEditorAPI();

// reject unrecognized api domain.
if (!editorAPI[domain]) return;

const domainAPIs = editorAPI[domain];
const overridedAPIs = Object.keys(handlers)
.filter((fnName) => !!domainAPIs[fnName])
.reduce((results, fnName) => {
results[fnName] = handlers[fnName];
return results;
}, {});

const newDomainAPIs = {
...domainAPIs,
...overridedAPIs,
};
editorAPI[domain] = newDomainAPIs as any;
}
1 change: 1 addition & 0 deletions Composer/packages/lib/shared/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from './sdk';
export * from './settings';
export * from './server';
export * from './shell';
export * from './EditorAPI';