From 368e0f0e8233c0f134e25eddc70d3a89b9f523bc Mon Sep 17 00:00:00 2001 From: zeye Date: Mon, 20 Jul 2020 15:57:15 +0800 Subject: [PATCH 01/15] draft: consume the EditorAPI in electron menu --- Composer/packages/electron-server/src/appMenu.ts | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/Composer/packages/electron-server/src/appMenu.ts b/Composer/packages/electron-server/src/appMenu.ts index e61e6d7a70..6461625b1f 100644 --- a/Composer/packages/electron-server/src/appMenu.ts +++ b/Composer/packages/electron-server/src/appMenu.ts @@ -29,7 +29,6 @@ function getAppMenu(): MenuItemConstructorOptions[] { function getRestOfEditMenu(): MenuItemConstructorOptions[] { if (isMac()) { return [ - { role: 'delete' }, { type: 'separator' }, { label: 'Speech', @@ -37,7 +36,7 @@ function getRestOfEditMenu(): MenuItemConstructorOptions[] { }, ]; } - return [{ role: 'delete' }, { type: 'separator' }, { role: 'selectAll' }]; + return [{ type: 'separator' }, { role: 'selectAll' }]; } function getRestOfWindowMenu(): MenuItemConstructorOptions[] { @@ -48,6 +47,8 @@ function getRestOfWindowMenu(): MenuItemConstructorOptions[] { } export function initAppMenu() { + // Global window functions registered by Composer core app. + const { EditorAPI } = window as any; const template: MenuItemConstructorOptions[] = [ // App (Mac) ...getAppMenu(), @@ -60,12 +61,13 @@ export function initAppMenu() { { label: 'Edit', submenu: [ - { role: 'undo' }, - { role: 'redo' }, + { role: 'undo', click: () => EditorAPI.undo() }, + { role: 'redo', click: () => EditorAPI.redo() }, { type: 'separator' }, - { role: 'cut' }, - { role: 'copy' }, - { role: 'paste' }, + { role: 'cut', click: () => EditorAPI.cutSelection() }, + { role: 'copy', click: () => EditorAPI.copySelection() }, + { role: 'paste', click: () => EditorAPI.pasteSelection() }, + { role: 'delete', click: () => EditorAPI.deleteSelection() }, ...getRestOfEditMenu(), ], }, From 0bcd51f309ee128ba6ae345f9796aa4314bae4af Mon Sep 17 00:00:00 2001 From: zeye Date: Tue, 28 Jul 2020 15:53:54 +0800 Subject: [PATCH 02/15] register global EditorAPI --- .../hooks/useEditorEventApi.ts | 17 ++--- .../lib/shared/src/types/EditorAPI.ts | 66 +++++++++++++++++++ .../packages/lib/shared/src/types/index.ts | 1 + 3 files changed, 76 insertions(+), 8 deletions(-) create mode 100644 Composer/packages/lib/shared/src/types/EditorAPI.ts diff --git a/Composer/packages/extensions/adaptive-flow/src/adaptive-flow-editor/hooks/useEditorEventApi.ts b/Composer/packages/extensions/adaptive-flow/src/adaptive-flow-editor/hooks/useEditorEventApi.ts index bbfa51d423..8739daa91a 100644 --- a/Composer/packages/extensions/adaptive-flow/src/adaptive-flow-editor/hooks/useEditorEventApi.ts +++ b/Composer/packages/extensions/adaptive-flow/src/adaptive-flow-editor/hooks/useEditorEventApi.ts @@ -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'; @@ -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, diff --git a/Composer/packages/lib/shared/src/types/EditorAPI.ts b/Composer/packages/lib/shared/src/types/EditorAPI.ts new file mode 100644 index 0000000000..0a458faf08 --- /dev/null +++ b/Composer/packages/lib/shared/src/types/EditorAPI.ts @@ -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; +} diff --git a/Composer/packages/lib/shared/src/types/index.ts b/Composer/packages/lib/shared/src/types/index.ts index 77dd246431..ab3827c46c 100644 --- a/Composer/packages/lib/shared/src/types/index.ts +++ b/Composer/packages/lib/shared/src/types/index.ts @@ -9,3 +9,4 @@ export * from './sdk'; export * from './settings'; export * from './server'; export * from './shell'; +export * from './EditorAPI'; From 1562869ebecb400faa0c0813faf11acfb2644a30 Mon Sep 17 00:00:00 2001 From: zeye Date: Tue, 28 Jul 2020 16:01:45 +0800 Subject: [PATCH 03/15] apply EditorAPI in ToolBar --- .../client/src/pages/design/DesignPage.tsx | 16 +++++----- .../client/src/pages/design/FrameAPI.ts | 31 ------------------- 2 files changed, 8 insertions(+), 39 deletions(-) delete mode 100644 Composer/packages/client/src/pages/design/FrameAPI.ts diff --git a/Composer/packages/client/src/pages/design/DesignPage.tsx b/Composer/packages/client/src/pages/design/DesignPage.tsx index 896533f84f..eb4bb64f74 100644 --- a/Composer/packages/client/src/pages/design/DesignPage.tsx +++ b/Composer/packages/client/src/pages/design/DesignPage.tsx @@ -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 } from '@bfc/shared'; +import { DialogFactory, SDKKinds, DialogInfo, PromptTab, getEditorAPI } from '@bfc/shared'; import { ActionButton } from 'office-ui-fabric-react/lib/Button'; import { JsonEditor } from '@bfc/code-editor'; import { useTriggerApi } from '@bfc/extension'; @@ -45,7 +45,6 @@ import { userSettingsState, } from '../../recoilModel'; -import { VisualEditorAPI } from './FrameAPI'; import { breadcrumbClass, contentWrapper, @@ -235,6 +234,7 @@ const DesignPage: React.FC { - VisualEditorAPI.cutSelection(); + EditorAPI.Actions.CutSelection(); }, }, { @@ -306,7 +306,7 @@ const DesignPage: React.FC { - VisualEditorAPI.copySelection(); + EditorAPI.Actions.CopySelection(); }, }, { @@ -314,7 +314,7 @@ const DesignPage: React.FC { - VisualEditorAPI.moveSelection(); + EditorAPI.Actions.MoveSelection(); }, }, { @@ -322,7 +322,7 @@ const DesignPage: React.FC { - VisualEditorAPI.deleteSelection(); + EditorAPI.Actions.DeleteSelection(); }, }, ], @@ -343,7 +343,7 @@ const DesignPage: React.FC { - VisualEditorAPI.disableSelection(); + EditorAPI.Actions.DisableSelection(); }, }, { @@ -351,7 +351,7 @@ const DesignPage: React.FC { - VisualEditorAPI.enableSelection(); + EditorAPI.Actions.EnableSelection(); }, }, ], diff --git a/Composer/packages/client/src/pages/design/FrameAPI.ts b/Composer/packages/client/src/pages/design/FrameAPI.ts deleted file mode 100644 index a26e168a3e..0000000000 --- a/Composer/packages/client/src/pages/design/FrameAPI.ts +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -export class FrameAPI { - /** - * Initialize the frame ref at first invocation. - */ - invoke = (method: string, ...rest: any[]) => { - if (typeof window[method] === 'function') { - return window[method](...rest); - } - }; -} - -export const VisualEditorAPI = (() => { - const visualEditorFrameAPI = new FrameAPI(); - // HACK: under cypress env, avoid invoking API inside frame too frequently (especially the `hasEleemntFocused`). It will lead to CI test quite fagile. - // TODO: remove this hack logic after refactoring state sync logic between shell and editors. - if ((window as any).Cypress) { - visualEditorFrameAPI.invoke = () => {}; - } - - return { - copySelection: () => visualEditorFrameAPI.invoke('copySelection'), - cutSelection: () => visualEditorFrameAPI.invoke('cutSelection'), - moveSelection: () => visualEditorFrameAPI.invoke('moveSelection'), - deleteSelection: () => visualEditorFrameAPI.invoke('deleteSelection'), - disableSelection: () => visualEditorFrameAPI.invoke('disableSelection'), - enableSelection: () => visualEditorFrameAPI.invoke('enableSelection'), - }; -})(); From 659e9823ca5260cc717f82ecb01a69ce16004f48 Mon Sep 17 00:00:00 2001 From: zeye Date: Tue, 28 Jul 2020 16:03:36 +0800 Subject: [PATCH 04/15] apply EditorAPI in Electron app menu --- Composer/packages/electron-server/src/appMenu.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Composer/packages/electron-server/src/appMenu.ts b/Composer/packages/electron-server/src/appMenu.ts index 6461625b1f..27789f0498 100644 --- a/Composer/packages/electron-server/src/appMenu.ts +++ b/Composer/packages/electron-server/src/appMenu.ts @@ -2,6 +2,7 @@ // Licensed under the MIT License. import { app, dialog, Menu, MenuItemConstructorOptions, shell } from 'electron'; +import { getEditorAPI } from '@bfc/shared'; import { isMac } from './utility/platform'; import { AppUpdater } from './appUpdater'; @@ -48,7 +49,7 @@ function getRestOfWindowMenu(): MenuItemConstructorOptions[] { export function initAppMenu() { // Global window functions registered by Composer core app. - const { EditorAPI } = window as any; + const EditorAPI = getEditorAPI(); const template: MenuItemConstructorOptions[] = [ // App (Mac) ...getAppMenu(), @@ -61,13 +62,12 @@ export function initAppMenu() { { label: 'Edit', submenu: [ - { role: 'undo', click: () => EditorAPI.undo() }, - { role: 'redo', click: () => EditorAPI.redo() }, + { role: 'undo', click: () => EditorAPI.Editing.Undo() }, + { role: 'redo', click: () => EditorAPI.Editing.Redo() }, { type: 'separator' }, - { role: 'cut', click: () => EditorAPI.cutSelection() }, - { role: 'copy', click: () => EditorAPI.copySelection() }, - { role: 'paste', click: () => EditorAPI.pasteSelection() }, - { role: 'delete', click: () => EditorAPI.deleteSelection() }, + { role: 'cut', click: () => EditorAPI.Actions.CutSelection() }, + { role: 'copy', click: () => EditorAPI.Actions.CopySelection() }, + { role: 'delete', click: () => EditorAPI.Actions.DeleteSelection() }, ...getRestOfEditMenu(), ], }, From 908c95ac602b2913f73711c79e32f36a3cfd2c31 Mon Sep 17 00:00:00 2001 From: zeye Date: Tue, 28 Jul 2020 16:05:14 +0800 Subject: [PATCH 05/15] add todos in undo/redo --- Composer/packages/client/src/pages/design/DesignPage.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Composer/packages/client/src/pages/design/DesignPage.tsx b/Composer/packages/client/src/pages/design/DesignPage.tsx index eb4bb64f74..3783f94c5b 100644 --- a/Composer/packages/client/src/pages/design/DesignPage.tsx +++ b/Composer/packages/client/src/pages/design/DesignPage.tsx @@ -282,6 +282,7 @@ const DesignPage: React.FC { + // TODO: register EditorAPI.Editing.Undo() //ToDo undo }, }, @@ -290,6 +291,7 @@ const DesignPage: React.FC { + // TODO: register EditorAPI.Editing.Redo() //ToDo redo }, }, From 0a7560f5004bf98b74fca0fce547e6cf0d360ff3 Mon Sep 17 00:00:00 2001 From: zeye Date: Tue, 28 Jul 2020 16:56:51 +0800 Subject: [PATCH 06/15] emit Electron menu events to renderer process --- .../packages/electron-server/src/appMenu.ts | 22 +++++++++++-------- Composer/packages/electron-server/src/main.ts | 2 +- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/Composer/packages/electron-server/src/appMenu.ts b/Composer/packages/electron-server/src/appMenu.ts index 27789f0498..7c76bbb071 100644 --- a/Composer/packages/electron-server/src/appMenu.ts +++ b/Composer/packages/electron-server/src/appMenu.ts @@ -2,7 +2,6 @@ // Licensed under the MIT License. import { app, dialog, Menu, MenuItemConstructorOptions, shell } from 'electron'; -import { getEditorAPI } from '@bfc/shared'; import { isMac } from './utility/platform'; import { AppUpdater } from './appUpdater'; @@ -47,9 +46,14 @@ function getRestOfWindowMenu(): MenuItemConstructorOptions[] { return [{ role: 'close' }]; } -export function initAppMenu() { - // Global window functions registered by Composer core app. - const EditorAPI = getEditorAPI(); +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', { event: menuEventName }); + } + }; + const template: MenuItemConstructorOptions[] = [ // App (Mac) ...getAppMenu(), @@ -62,12 +66,12 @@ export function initAppMenu() { { label: 'Edit', submenu: [ - { role: 'undo', click: () => EditorAPI.Editing.Undo() }, - { role: 'redo', click: () => EditorAPI.Editing.Redo() }, + { role: 'undo', click: () => handleMenuEvents('undo') }, + { role: 'redo', click: () => handleMenuEvents('redo') }, { type: 'separator' }, - { role: 'cut', click: () => EditorAPI.Actions.CutSelection() }, - { role: 'copy', click: () => EditorAPI.Actions.CopySelection() }, - { role: 'delete', click: () => EditorAPI.Actions.DeleteSelection() }, + { role: 'cut', click: () => handleMenuEvents('cut') }, + { role: 'copy', click: () => handleMenuEvents('copy') }, + { role: 'delete', click: () => handleMenuEvents('delete') }, ...getRestOfEditMenu(), ], }, diff --git a/Composer/packages/electron-server/src/main.ts b/Composer/packages/electron-server/src/main.ts index 79374bb772..4082227967 100644 --- a/Composer/packages/electron-server/src/main.ts +++ b/Composer/packages/electron-server/src/main.ts @@ -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(); From df8e6f25be8b6d6ad179ea0dfaf1336cc96007e4 Mon Sep 17 00:00:00 2001 From: zeye Date: Tue, 28 Jul 2020 19:09:09 +0800 Subject: [PATCH 07/15] use ipc channel to subscribe Electron menu events --- .../client/src/pages/design/DesignPage.tsx | 23 +++++++++++++++++++ .../packages/electron-server/src/appMenu.ts | 2 +- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/Composer/packages/client/src/pages/design/DesignPage.tsx b/Composer/packages/client/src/pages/design/DesignPage.tsx index 3783f94c5b..7d8457d33c 100644 --- a/Composer/packages/client/src/pages/design/DesignPage.tsx +++ b/Composer/packages/client/src/pages/design/DesignPage.tsx @@ -181,6 +181,29 @@ const DesignPage: React.FC { + const EditorAPI = getEditorAPI(); + if (!window.__IS_ELECTRON__) return; + if (!window.ipcRenderer || typeof window.ipcRenderer.on !== 'function') return; + + 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(); + } + }); + }, []); + const onTriggerCreationDismiss = () => { setTriggerModalVisibility(false); }; diff --git a/Composer/packages/electron-server/src/appMenu.ts b/Composer/packages/electron-server/src/appMenu.ts index 7c76bbb071..d84201cf36 100644 --- a/Composer/packages/electron-server/src/appMenu.ts +++ b/Composer/packages/electron-server/src/appMenu.ts @@ -50,7 +50,7 @@ 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', { event: menuEventName }); + win.webContents.send('electron-menu-clicked', { l: menuEventName }); } }; From 58654a01fc54a391f32f13f737a6e294bfb92c9b Mon Sep 17 00:00:00 2001 From: zeye Date: Tue, 28 Jul 2020 21:03:37 +0800 Subject: [PATCH 08/15] override default menu event --- Composer/packages/electron-server/src/appMenu.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/Composer/packages/electron-server/src/appMenu.ts b/Composer/packages/electron-server/src/appMenu.ts index d84201cf36..6d270d4d6b 100644 --- a/Composer/packages/electron-server/src/appMenu.ts +++ b/Composer/packages/electron-server/src/appMenu.ts @@ -50,7 +50,7 @@ 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', { l: menuEventName }); + win.webContents.send('electron-menu-clicked', { label: menuEventName }); } }; @@ -66,12 +66,13 @@ export function initAppMenu(win?: Electron.BrowserWindow) { { label: 'Edit', submenu: [ - { role: 'undo', click: () => handleMenuEvents('undo') }, - { role: 'redo', click: () => handleMenuEvents('redo') }, + // NOTE: Avoid using builtin `role`, it won't override the click handler. + { label: 'Undo', click: () => handleMenuEvents('undo') }, + { label: 'Redo', click: () => handleMenuEvents('redo') }, { type: 'separator' }, - { role: 'cut', click: () => handleMenuEvents('cut') }, - { role: 'copy', click: () => handleMenuEvents('copy') }, - { role: 'delete', click: () => handleMenuEvents('delete') }, + { label: 'Cut', click: () => handleMenuEvents('cut') }, + { label: 'Copy', click: () => handleMenuEvents('copy') }, + { label: 'Delete', click: () => handleMenuEvents('delete') }, ...getRestOfEditMenu(), ], }, From 1e2938c2305e2f93283821078ac35657b376ec45 Mon Sep 17 00:00:00 2001 From: zeye Date: Tue, 28 Jul 2020 21:07:10 +0800 Subject: [PATCH 09/15] add shortcuts --- Composer/packages/electron-server/src/appMenu.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Composer/packages/electron-server/src/appMenu.ts b/Composer/packages/electron-server/src/appMenu.ts index 6d270d4d6b..bcc7c14bf0 100644 --- a/Composer/packages/electron-server/src/appMenu.ts +++ b/Composer/packages/electron-server/src/appMenu.ts @@ -67,12 +67,12 @@ export function initAppMenu(win?: Electron.BrowserWindow) { label: 'Edit', submenu: [ // NOTE: Avoid using builtin `role`, it won't override the click handler. - { label: 'Undo', click: () => handleMenuEvents('undo') }, - { label: 'Redo', click: () => handleMenuEvents('redo') }, + { label: 'Undo', accelerator: 'CmdOrCtrl+Z', click: () => handleMenuEvents('undo') }, + { label: 'Redo', accelerator: 'CmdOrCtrl+Y', click: () => handleMenuEvents('redo') }, { type: 'separator' }, - { label: 'Cut', click: () => handleMenuEvents('cut') }, - { label: 'Copy', click: () => handleMenuEvents('copy') }, - { label: 'Delete', click: () => handleMenuEvents('delete') }, + { label: 'Cut', accelerator: 'CmdOrCtrl+X', click: () => handleMenuEvents('cut') }, + { label: 'Copy', accelerator: 'CmdOrCtrl+C', click: () => handleMenuEvents('copy') }, + { label: 'Delete', accelerator: 'Delete', click: () => handleMenuEvents('delete') }, ...getRestOfEditMenu(), ], }, From 139171dc69338ee264adaed62c3fbedc092c249a Mon Sep 17 00:00:00 2001 From: zeye Date: Tue, 28 Jul 2020 21:11:47 +0800 Subject: [PATCH 10/15] remove duplicated usage of getEditorApi() --- Composer/packages/client/src/pages/design/DesignPage.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Composer/packages/client/src/pages/design/DesignPage.tsx b/Composer/packages/client/src/pages/design/DesignPage.tsx index 7d8457d33c..6173dac77e 100644 --- a/Composer/packages/client/src/pages/design/DesignPage.tsx +++ b/Composer/packages/client/src/pages/design/DesignPage.tsx @@ -181,9 +181,9 @@ const DesignPage: React.FC { - const EditorAPI = getEditorAPI(); if (!window.__IS_ELECTRON__) return; if (!window.ipcRenderer || typeof window.ipcRenderer.on !== 'function') return; @@ -257,7 +257,6 @@ const DesignPage: React.FC Date: Tue, 28 Jul 2020 21:28:28 +0800 Subject: [PATCH 11/15] fix UT --- Composer/packages/electron-server/__tests__/appMenu.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Composer/packages/electron-server/__tests__/appMenu.test.ts b/Composer/packages/electron-server/__tests__/appMenu.test.ts index 7d61261100..3227f04f96 100644 --- a/Composer/packages/electron-server/__tests__/appMenu.test.ts +++ b/Composer/packages/electron-server/__tests__/appMenu.test.ts @@ -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'); @@ -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'); From 1f89459f8b39a8b62c9b8fa6590698c2ebd014ec Mon Sep 17 00:00:00 2001 From: zeye Date: Thu, 30 Jul 2020 16:32:13 +0800 Subject: [PATCH 12/15] update 'Redo' shortcut in Electron menu --- Composer/packages/electron-server/src/appMenu.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Composer/packages/electron-server/src/appMenu.ts b/Composer/packages/electron-server/src/appMenu.ts index bcc7c14bf0..189c631bb1 100644 --- a/Composer/packages/electron-server/src/appMenu.ts +++ b/Composer/packages/electron-server/src/appMenu.ts @@ -68,7 +68,7 @@ export function initAppMenu(win?: Electron.BrowserWindow) { submenu: [ // NOTE: Avoid using builtin `role`, it won't override the click handler. { label: 'Undo', accelerator: 'CmdOrCtrl+Z', click: () => handleMenuEvents('undo') }, - { label: 'Redo', accelerator: 'CmdOrCtrl+Y', click: () => handleMenuEvents('redo') }, + { label: 'Redo', accelerator: 'CmdOrCtrl+Shift+Z', click: () => handleMenuEvents('redo') }, { type: 'separator' }, { label: 'Cut', accelerator: 'CmdOrCtrl+X', click: () => handleMenuEvents('cut') }, { label: 'Copy', accelerator: 'CmdOrCtrl+C', click: () => handleMenuEvents('copy') }, From bb675a333f337bba41f98060f9be8c7dccb5af60 Mon Sep 17 00:00:00 2001 From: zeye Date: Thu, 30 Jul 2020 18:05:51 +0800 Subject: [PATCH 13/15] disable cut/copy/del menu when no action selected --- .../client/src/pages/design/DesignPage.tsx | 54 +++++++++++-------- .../packages/electron-server/src/appMenu.ts | 33 +++++++++--- 2 files changed, 58 insertions(+), 29 deletions(-) diff --git a/Composer/packages/client/src/pages/design/DesignPage.tsx b/Composer/packages/client/src/pages/design/DesignPage.tsx index 6173dac77e..70866d0bab 100644 --- a/Composer/packages/client/src/pages/design/DesignPage.tsx +++ b/Composer/packages/client/src/pages/design/DesignPage.tsx @@ -181,29 +181,6 @@ const DesignPage: React.FC { - if (!window.__IS_ELECTRON__) return; - if (!window.ipcRenderer || typeof window.ipcRenderer.on !== 'function') return; - - 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(); - } - }); - }, []); - const onTriggerCreationDismiss = () => { setTriggerModalVisibility(false); }; @@ -257,6 +234,37 @@ const DesignPage: React.FC { + if (!window.__IS_ELECTRON__) return; + if (!window.ipcRenderer || typeof window.ipcRenderer.send !== 'function') return; + + window.ipcRenderer.send('composer-state-change', { actionSelected }); + }, [actionSelected]); + + const EditorAPI = getEditorAPI(); + // 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; + + 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(); + } + }); + }, []); + const toolbarItems: IToolbarItem[] = [ { type: 'dropdown', diff --git a/Composer/packages/electron-server/src/appMenu.ts b/Composer/packages/electron-server/src/appMenu.ts index 189c631bb1..c173d5acc0 100644 --- a/Composer/packages/electron-server/src/appMenu.ts +++ b/Composer/packages/electron-server/src/appMenu.ts @@ -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'; @@ -67,12 +67,24 @@ export function initAppMenu(win?: Electron.BrowserWindow) { label: 'Edit', submenu: [ // NOTE: Avoid using builtin `role`, it won't override the click handler. - { label: 'Undo', accelerator: 'CmdOrCtrl+Z', click: () => handleMenuEvents('undo') }, - { label: 'Redo', accelerator: 'CmdOrCtrl+Shift+Z', click: () => handleMenuEvents('redo') }, + { id: 'Undo', label: 'Undo', accelerator: 'CmdOrCtrl+Z', click: () => handleMenuEvents('undo') }, + { id: 'Redo', label: 'Redo', accelerator: 'CmdOrCtrl+Shift+Z', click: () => handleMenuEvents('redo') }, { type: 'separator' }, - { label: 'Cut', accelerator: 'CmdOrCtrl+X', click: () => handleMenuEvents('cut') }, - { label: 'Copy', accelerator: 'CmdOrCtrl+C', click: () => handleMenuEvents('copy') }, - { label: 'Delete', accelerator: 'Delete', click: () => handleMenuEvents('delete') }, + { 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(), ], }, @@ -168,4 +180,13 @@ export function initAppMenu(win?: Electron.BrowserWindow) { const menu = Menu.buildFromTemplate(template); Menu.setApplicationMenu(menu); + + // Let menu enable/disable status reflect action selection states. + ipcMain.on('composer-state-change', (e, state) => { + const actionSelected = !!state.actionSelected; + ['Cut', 'Copy', 'Delete'].forEach((id) => { + menu.getMenuItemById(id).enabled = actionSelected; + }); + Menu.setApplicationMenu(menu); + }); } From 8cc6b90b19f6fe68a83fdb4d5a6179bfb8045a01 Mon Sep 17 00:00:00 2001 From: zeye Date: Thu, 30 Jul 2020 18:18:02 +0800 Subject: [PATCH 14/15] extract Electron logic to a hook --- .../client/src/hooks/useElectronFeatures.ts | 39 +++++++++++++++++++ .../client/src/pages/design/DesignPage.tsx | 33 ++-------------- 2 files changed, 42 insertions(+), 30 deletions(-) create mode 100644 Composer/packages/client/src/hooks/useElectronFeatures.ts diff --git a/Composer/packages/client/src/hooks/useElectronFeatures.ts b/Composer/packages/client/src/hooks/useElectronFeatures.ts new file mode 100644 index 0000000000..3f5846eba2 --- /dev/null +++ b/Composer/packages/client/src/hooks/useElectronFeatures.ts @@ -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(); + } + }); + }, []); +}; diff --git a/Composer/packages/client/src/pages/design/DesignPage.tsx b/Composer/packages/client/src/pages/design/DesignPage.tsx index 70866d0bab..e50b52680f 100644 --- a/Composer/packages/client/src/pages/design/DesignPage.tsx +++ b/Composer/packages/client/src/pages/design/DesignPage.tsx @@ -44,6 +44,7 @@ import { actionsSeedState, userSettingsState, } from '../../recoilModel'; +import { useElectronFeatures } from '../../hooks/useElectronFeatures'; import { breadcrumbClass, @@ -226,7 +227,7 @@ const DesignPage: React.FC { 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); @@ -234,37 +235,9 @@ const DesignPage: React.FC { - if (!window.__IS_ELECTRON__) return; - if (!window.ipcRenderer || typeof window.ipcRenderer.send !== 'function') return; - - window.ipcRenderer.send('composer-state-change', { actionSelected }); - }, [actionSelected]); + useElectronFeatures(actionSelected); const EditorAPI = getEditorAPI(); - // 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; - - 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(); - } - }); - }, []); - const toolbarItems: IToolbarItem[] = [ { type: 'dropdown', From b93c9ab14cb213ec70ecd12515a917dd0e0f1ba9 Mon Sep 17 00:00:00 2001 From: zeye Date: Fri, 31 Jul 2020 11:38:51 +0800 Subject: [PATCH 15/15] check ipcMain in jest env --- Composer/packages/electron-server/src/appMenu.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/Composer/packages/electron-server/src/appMenu.ts b/Composer/packages/electron-server/src/appMenu.ts index c173d5acc0..f6e877c441 100644 --- a/Composer/packages/electron-server/src/appMenu.ts +++ b/Composer/packages/electron-server/src/appMenu.ts @@ -182,11 +182,13 @@ export function initAppMenu(win?: Electron.BrowserWindow) { Menu.setApplicationMenu(menu); // Let menu enable/disable status reflect action selection states. - ipcMain.on('composer-state-change', (e, state) => { - const actionSelected = !!state.actionSelected; - ['Cut', 'Copy', 'Delete'].forEach((id) => { - menu.getMenuItemById(id).enabled = actionSelected; + 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); }); - Menu.setApplicationMenu(menu); - }); }