mirror of
https://github.com/finos/SymphonyElectron.git
synced 2024-12-27 17:31:36 -06:00
SDA-3931: Snipping tools improvement
This commit is contained in:
parent
65505c0ccd
commit
fad3656536
21
spec/__snapshots__/menu-button.spec.tsx.snap
Normal file
21
spec/__snapshots__/menu-button.spec.tsx.snap
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
|
exports[`Menu Button should show all elements 1`] = `
|
||||||
|
<Fragment>
|
||||||
|
<div
|
||||||
|
className="menu-button-wrapper"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
className="menu-button "
|
||||||
|
data-testid="undefined_MENU_BUTTON"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
alt="Open menu"
|
||||||
|
src="../renderer/assets/single-chevron-down.svg"
|
||||||
|
title="Open menu"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</Fragment>
|
||||||
|
`;
|
@ -86,12 +86,38 @@ exports[`Snipping Tool should render correctly 1`] = `
|
|||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>
|
||||||
<button
|
<button
|
||||||
className="done-button"
|
className="close-button"
|
||||||
|
data-testid="close-button"
|
||||||
|
onClick={[Function]}
|
||||||
|
>
|
||||||
|
Close
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className="add-to-chat-button"
|
||||||
data-testid="done-button"
|
data-testid="done-button"
|
||||||
onClick={[Function]}
|
onClick={[Function]}
|
||||||
>
|
>
|
||||||
Done
|
Add to chat
|
||||||
</button>
|
</button>
|
||||||
|
<MenuButton
|
||||||
|
id="snipping-tool"
|
||||||
|
listItems={
|
||||||
|
Array [
|
||||||
|
Object {
|
||||||
|
"dataTestId": "COPY_TO_CLIPBOARD",
|
||||||
|
"event": "copy-to-clipboard",
|
||||||
|
"name": "Copy to clipboard",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
|
Object {
|
||||||
|
"dataTestId": "SAVE_AS",
|
||||||
|
"event": "save-as",
|
||||||
|
"name": "Save as",
|
||||||
|
"onClick": [Function],
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
/>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
|
91
spec/menu-button.spec.tsx
Normal file
91
spec/menu-button.spec.tsx
Normal file
@ -0,0 +1,91 @@
|
|||||||
|
import { mount, shallow } from 'enzyme';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { ScreenShotAnnotation } from '../src/common/ipcEvent';
|
||||||
|
import MenuButton from '../src/renderer/components/menu-button';
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Menu Button', () => {
|
||||||
|
//#region Logic
|
||||||
|
const waitForPromisesToResolve = () =>
|
||||||
|
new Promise((resolve) => setTimeout(resolve));
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region Mock
|
||||||
|
const copyToClipboardFn = jest.fn();
|
||||||
|
const saveAsFn = jest.fn();
|
||||||
|
copyToClipboardFn.mockImplementation((event) => {
|
||||||
|
return event;
|
||||||
|
});
|
||||||
|
saveAsFn.mockImplementation((event) => {
|
||||||
|
return event;
|
||||||
|
});
|
||||||
|
const menuItem = [
|
||||||
|
{
|
||||||
|
name: 'Copy to clipboard',
|
||||||
|
event: ScreenShotAnnotation.COPY_TO_CLIPBOARD,
|
||||||
|
onClick: copyToClipboardFn,
|
||||||
|
dataTestId: 'COPY_TO_CLIPBOARD',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Save as',
|
||||||
|
event: ScreenShotAnnotation.SAVE_AS,
|
||||||
|
onClick: saveAsFn,
|
||||||
|
dataTestId: 'SAVE_AS',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
it('should show all elements', () => {
|
||||||
|
const wrapper = shallow(React.createElement(MenuButton));
|
||||||
|
expect(wrapper).toMatchSnapshot();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call event on click copy to clipboard', async () => {
|
||||||
|
const wrapper = mount(
|
||||||
|
<MenuButton id='snipping-tool' listItems={menuItem} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
wrapper.find('[data-testid="snipping-tool_MENU_BUTTON"]').simulate('click');
|
||||||
|
wrapper.update();
|
||||||
|
await waitForPromisesToResolve();
|
||||||
|
wrapper
|
||||||
|
.find('[data-testid="snipping-tool_COPY_TO_CLIPBOARD"]')
|
||||||
|
.simulate('click');
|
||||||
|
wrapper.update();
|
||||||
|
await waitForPromisesToResolve();
|
||||||
|
expect(copyToClipboardFn).toBeCalledWith(
|
||||||
|
ScreenShotAnnotation.COPY_TO_CLIPBOARD,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call event on click save as', async () => {
|
||||||
|
const wrapper = mount(
|
||||||
|
<MenuButton id='snipping-tool' listItems={menuItem} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
wrapper.find('[data-testid="snipping-tool_MENU_BUTTON"]').simulate('click');
|
||||||
|
wrapper.update();
|
||||||
|
await waitForPromisesToResolve();
|
||||||
|
wrapper.find('[data-testid="snipping-tool_SAVE_AS"]').simulate('click');
|
||||||
|
wrapper.update();
|
||||||
|
await waitForPromisesToResolve();
|
||||||
|
expect(saveAsFn).toBeCalledWith(ScreenShotAnnotation.SAVE_AS);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should disappear on click to other element', async () => {
|
||||||
|
const wrapper = mount(
|
||||||
|
<MenuButton id='snipping-tool' listItems={menuItem} />,
|
||||||
|
);
|
||||||
|
|
||||||
|
wrapper.find('[data-testid="snipping-tool_MENU_BUTTON"]').simulate('click');
|
||||||
|
wrapper.update();
|
||||||
|
await waitForPromisesToResolve();
|
||||||
|
wrapper.find('[data-testid="snipping-tool_MENU_BUTTON"]').simulate('click');
|
||||||
|
wrapper.update();
|
||||||
|
await waitForPromisesToResolve();
|
||||||
|
expect(wrapper.find('.menu').exists()).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
@ -1,5 +1,14 @@
|
|||||||
|
jest.mock('save-svg-as-png', function () {
|
||||||
|
return {
|
||||||
|
svgAsPngUri: async function (svg) {
|
||||||
|
return Promise.resolve(svg);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
import { mount, shallow } from 'enzyme';
|
import { mount, shallow } from 'enzyme';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
import { ScreenShotAnnotation } from '../src/common/ipcEvent';
|
||||||
import SnippingTool from '../src/renderer/components/snipping-tool';
|
import SnippingTool from '../src/renderer/components/snipping-tool';
|
||||||
import { ipcRenderer } from './__mocks__/electron';
|
import { ipcRenderer } from './__mocks__/electron';
|
||||||
|
|
||||||
@ -112,4 +121,61 @@ describe('Snipping Tool', () => {
|
|||||||
screenSnippetPath: '',
|
screenSnippetPath: '',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should send upload-snippet event with correct data when clicked on copy to clipboard', async () => {
|
||||||
|
const wrapper = mount(<SnippingTool />);
|
||||||
|
const spy = jest.spyOn(ipcRenderer, 'send');
|
||||||
|
jest.spyOn(document, 'getElementById').mockImplementation((selector) => {
|
||||||
|
switch (selector) {
|
||||||
|
case 'annotate-area':
|
||||||
|
return '123';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
wrapper.find('[data-testid="snipping-tool_MENU_BUTTON"]').simulate('click');
|
||||||
|
wrapper.update();
|
||||||
|
await waitForPromisesToResolve();
|
||||||
|
wrapper
|
||||||
|
.find('[data-testid="snipping-tool_COPY_TO_CLIPBOARD"]')
|
||||||
|
.simulate('click');
|
||||||
|
|
||||||
|
wrapper.update();
|
||||||
|
await waitForPromisesToResolve();
|
||||||
|
expect(spy).toBeCalledWith(ScreenShotAnnotation.COPY_TO_CLIPBOARD, {
|
||||||
|
clipboard: '123',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send upload-snippet event with correct data when clicked on save as', async () => {
|
||||||
|
const wrapper = mount(<SnippingTool />);
|
||||||
|
const spy = jest.spyOn(ipcRenderer, 'send');
|
||||||
|
jest.spyOn(document, 'getElementById').mockImplementation((selector) => {
|
||||||
|
switch (selector) {
|
||||||
|
case 'annotate-area':
|
||||||
|
return '123';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
wrapper.find('[data-testid="snipping-tool_MENU_BUTTON"]').simulate('click');
|
||||||
|
wrapper.update();
|
||||||
|
await waitForPromisesToResolve();
|
||||||
|
wrapper.find('[data-testid="snipping-tool_SAVE_AS"]').simulate('click');
|
||||||
|
wrapper.update();
|
||||||
|
await waitForPromisesToResolve();
|
||||||
|
expect(spy).toBeCalledWith(ScreenShotAnnotation.SAVE_AS, {
|
||||||
|
clipboard: '123',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send upload-snippet event with correct data when clicked on close', async () => {
|
||||||
|
const wrapper = mount(<SnippingTool />);
|
||||||
|
const spy = jest.spyOn(ipcRenderer, 'send');
|
||||||
|
wrapper.find('[data-testid="close-button"]').simulate('click');
|
||||||
|
wrapper.update();
|
||||||
|
await waitForPromisesToResolve();
|
||||||
|
expect(spy).toBeCalledWith(ScreenShotAnnotation.CLOSE);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
app,
|
app,
|
||||||
BrowserWindow,
|
BrowserWindow,
|
||||||
|
clipboard,
|
||||||
|
dialog,
|
||||||
ipcMain,
|
ipcMain,
|
||||||
nativeImage,
|
nativeImage,
|
||||||
WebContents,
|
WebContents,
|
||||||
@ -20,6 +22,7 @@ import {
|
|||||||
isWindowsOS,
|
isWindowsOS,
|
||||||
} from '../common/env';
|
} from '../common/env';
|
||||||
import { i18n } from '../common/i18n';
|
import { i18n } from '../common/i18n';
|
||||||
|
import { ScreenShotAnnotation } from '../common/ipcEvent';
|
||||||
import { logger } from '../common/logger';
|
import { logger } from '../common/logger';
|
||||||
import {
|
import {
|
||||||
analytics,
|
analytics,
|
||||||
@ -27,11 +30,18 @@ import {
|
|||||||
ScreenSnippetActionTypes,
|
ScreenSnippetActionTypes,
|
||||||
} from './analytics-handler';
|
} from './analytics-handler';
|
||||||
import { updateAlwaysOnTop } from './window-actions';
|
import { updateAlwaysOnTop } from './window-actions';
|
||||||
import { windowHandler } from './window-handler';
|
import { ICustomBrowserWindow, windowHandler } from './window-handler';
|
||||||
import { windowExists } from './window-utils';
|
import { windowExists } from './window-utils';
|
||||||
|
|
||||||
const readFile = util.promisify(fs.readFile);
|
const readFile = util.promisify(fs.readFile);
|
||||||
|
|
||||||
|
export interface IListItem {
|
||||||
|
name: string;
|
||||||
|
event: string;
|
||||||
|
dataTestId: string;
|
||||||
|
onClick: (eventName: string) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
class ScreenSnippet {
|
class ScreenSnippet {
|
||||||
private readonly tempDir: string;
|
private readonly tempDir: string;
|
||||||
private outputFilePath: string | undefined;
|
private outputFilePath: string | undefined;
|
||||||
@ -161,8 +171,18 @@ class ScreenSnippet {
|
|||||||
}
|
}
|
||||||
|
|
||||||
windowHandler.closeSnippingToolWindow();
|
windowHandler.closeSnippingToolWindow();
|
||||||
windowHandler.createSnippingToolWindow(this.outputFilePath, dimensions);
|
const windowName = this.focusedWindow
|
||||||
|
? (this.focusedWindow as ICustomBrowserWindow).winName
|
||||||
|
: '';
|
||||||
|
windowHandler.createSnippingToolWindow(
|
||||||
|
this.outputFilePath,
|
||||||
|
dimensions,
|
||||||
|
windowName,
|
||||||
|
);
|
||||||
this.uploadSnippet(webContents);
|
this.uploadSnippet(webContents);
|
||||||
|
this.closeSnippet();
|
||||||
|
this.copyToClipboard();
|
||||||
|
this.saveAs();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const {
|
const {
|
||||||
@ -194,7 +214,6 @@ class ScreenSnippet {
|
|||||||
this.focusedWindow = BrowserWindow.getFocusedWindow();
|
this.focusedWindow = BrowserWindow.getFocusedWindow();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.execCmd(this.captureUtil, []);
|
|
||||||
await this.verifyAndUpdateAlwaysOnTop();
|
await this.verifyAndUpdateAlwaysOnTop();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
await this.verifyAndUpdateAlwaysOnTop();
|
await this.verifyAndUpdateAlwaysOnTop();
|
||||||
@ -343,6 +362,124 @@ class ScreenSnippet {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the current snippet
|
||||||
|
*/
|
||||||
|
private closeSnippet() {
|
||||||
|
ipcMain.once(ScreenShotAnnotation.CLOSE, async (_event) => {
|
||||||
|
try {
|
||||||
|
windowHandler.closeSnippingToolWindow();
|
||||||
|
await this.verifyAndUpdateAlwaysOnTop();
|
||||||
|
} catch (error) {
|
||||||
|
await this.verifyAndUpdateAlwaysOnTop();
|
||||||
|
logger.error(
|
||||||
|
`screen-snippet-handler: close window failed with error: ${error}!`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cancels a screen capture and closes the snippet window
|
||||||
|
*/
|
||||||
|
private copyToClipboard() {
|
||||||
|
ipcMain.on(
|
||||||
|
ScreenShotAnnotation.COPY_TO_CLIPBOARD,
|
||||||
|
async (
|
||||||
|
_event,
|
||||||
|
copyToClipboardData: {
|
||||||
|
action: string;
|
||||||
|
clipboard: string;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
logger.info(`screen-snippet-handler: Copied!`);
|
||||||
|
this.focusedWindow = BrowserWindow.getFocusedWindow();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const [, data] = copyToClipboardData.clipboard.split(',');
|
||||||
|
|
||||||
|
const buffer = Buffer.from(data, 'base64');
|
||||||
|
const img = nativeImage.createFromBuffer(buffer);
|
||||||
|
|
||||||
|
clipboard.writeImage(img);
|
||||||
|
|
||||||
|
await this.verifyAndUpdateAlwaysOnTop();
|
||||||
|
} catch (error) {
|
||||||
|
await this.verifyAndUpdateAlwaysOnTop();
|
||||||
|
logger.error(
|
||||||
|
`screen-snippet-handler: cannot copy, failed with error: ${error}!`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger save modal to save the snippet
|
||||||
|
*/
|
||||||
|
private saveAs() {
|
||||||
|
ipcMain.on(
|
||||||
|
ScreenShotAnnotation.SAVE_AS,
|
||||||
|
async (
|
||||||
|
_event,
|
||||||
|
saveAsData: {
|
||||||
|
clipboard: string;
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
const filePath = path.join(
|
||||||
|
this.tempDir,
|
||||||
|
'symphonyImage-' + Date.now() + '.png',
|
||||||
|
);
|
||||||
|
const [, data] = saveAsData.clipboard.split(',');
|
||||||
|
const buffer = Buffer.from(data, 'base64');
|
||||||
|
const img = nativeImage.createFromBuffer(buffer);
|
||||||
|
|
||||||
|
const dialogResult = await dialog
|
||||||
|
.showSaveDialog(BrowserWindow.getFocusedWindow() as BrowserWindow, {
|
||||||
|
title: 'Select place to store your file',
|
||||||
|
defaultPath: filePath,
|
||||||
|
// defaultPath: path.join(__dirname, '../assets/'),
|
||||||
|
buttonLabel: 'Save',
|
||||||
|
// Restricting the user to only Text Files.
|
||||||
|
filters: [
|
||||||
|
{
|
||||||
|
name: 'Image file',
|
||||||
|
extensions: ['png'],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
properties: [],
|
||||||
|
})
|
||||||
|
.then((file) => {
|
||||||
|
// Stating whether dialog operation was cancelled or not.
|
||||||
|
if (!file.canceled && file.filePath) {
|
||||||
|
// Creating and Writing to the sample.txt file
|
||||||
|
fs.writeFile(file.filePath.toString(), img.toPNG(), (err) => {
|
||||||
|
if (err) {
|
||||||
|
throw logger.error(
|
||||||
|
`screen-snippet-handler: cannot save file, failed with error: ${err}!`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`screen-snippet-handler: modal save opened!`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return file;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
logger.error(
|
||||||
|
`screen-snippet-handler: cannot save file, failed with error: ${err}!`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
});
|
||||||
|
if (dialogResult?.filePath) {
|
||||||
|
windowHandler.closeSnippingToolWindow();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const screenSnippet = new ScreenSnippet();
|
const screenSnippet = new ScreenSnippet();
|
||||||
|
@ -20,6 +20,7 @@ import { format, parse } from 'url';
|
|||||||
import { apiName, Themes, WindowTypes } from '../common/api-interface';
|
import { apiName, Themes, WindowTypes } from '../common/api-interface';
|
||||||
import { isDevEnv, isLinux, isMac, isWindowsOS } from '../common/env';
|
import { isDevEnv, isLinux, isMac, isWindowsOS } from '../common/env';
|
||||||
import { i18n, LocaleType } from '../common/i18n';
|
import { i18n, LocaleType } from '../common/i18n';
|
||||||
|
import { ScreenShotAnnotation } from '../common/ipcEvent';
|
||||||
import { logger } from '../common/logger';
|
import { logger } from '../common/logger';
|
||||||
import {
|
import {
|
||||||
calculatePercentage,
|
calculatePercentage,
|
||||||
@ -1101,6 +1102,7 @@ export class WindowHandler {
|
|||||||
height: number;
|
height: number;
|
||||||
width: number;
|
width: number;
|
||||||
},
|
},
|
||||||
|
windowName: string,
|
||||||
): void {
|
): void {
|
||||||
// Prevents creating multiple instances
|
// Prevents creating multiple instances
|
||||||
if (didVerifyAndRestoreWindow(this.snippingToolWindow)) {
|
if (didVerifyAndRestoreWindow(this.snippingToolWindow)) {
|
||||||
@ -1184,11 +1186,13 @@ export class WindowHandler {
|
|||||||
toolWidth = scaledImageDimensions.width;
|
toolWidth = scaledImageDimensions.width;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const selectedParentWindow = getWindowByName(windowName);
|
||||||
const opts: ICustomBrowserWindowConstructorOpts = this.getWindowOpts(
|
const opts: ICustomBrowserWindowConstructorOpts = this.getWindowOpts(
|
||||||
{
|
{
|
||||||
width: toolWidth,
|
width: toolWidth,
|
||||||
height: toolHeight,
|
height: toolHeight,
|
||||||
modal: false,
|
parent: selectedParentWindow,
|
||||||
|
modal: true,
|
||||||
alwaysOnTop: false,
|
alwaysOnTop: false,
|
||||||
resizable: false,
|
resizable: false,
|
||||||
fullscreenable: false,
|
fullscreenable: false,
|
||||||
@ -1270,6 +1274,9 @@ export class WindowHandler {
|
|||||||
logger.info(
|
logger.info(
|
||||||
'window-handler, createSnippingToolWindow: Closing snipping window, attempting to delete temp snip image',
|
'window-handler, createSnippingToolWindow: Closing snipping window, attempting to delete temp snip image',
|
||||||
);
|
);
|
||||||
|
ipcMain.removeAllListeners(ScreenShotAnnotation.COPY_TO_CLIPBOARD);
|
||||||
|
ipcMain.removeAllListeners(ScreenShotAnnotation.SAVE_AS);
|
||||||
|
this.snippingToolWindow?.close();
|
||||||
this.deleteFile(snipImage);
|
this.deleteFile(snipImage);
|
||||||
this.removeWindow(opts.winKey);
|
this.removeWindow(opts.winKey);
|
||||||
this.screenPickerWindow = null;
|
this.screenPickerWindow = null;
|
||||||
|
5
src/common/ipcEvent.ts
Normal file
5
src/common/ipcEvent.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export enum ScreenShotAnnotation {
|
||||||
|
COPY_TO_CLIPBOARD = 'copy-to-clipboard',
|
||||||
|
SAVE_AS = 'save-as',
|
||||||
|
CLOSE = 'close-snippet',
|
||||||
|
}
|
@ -160,7 +160,11 @@
|
|||||||
"You are sharing your screen on {appName}": "You are sharing your screen on {appName}"
|
"You are sharing your screen on {appName}": "You are sharing your screen on {appName}"
|
||||||
},
|
},
|
||||||
"ScreenSnippet": {
|
"ScreenSnippet": {
|
||||||
"Done": "Done",
|
"Add to chat": "ADD TO CHAT",
|
||||||
|
"Open menu": "Open menu",
|
||||||
|
"Copy to clipboard": "Copy to clipboard",
|
||||||
|
"Save as": "Save as",
|
||||||
|
"Close": "Close",
|
||||||
"Erase": "Erase",
|
"Erase": "Erase",
|
||||||
"Highlight": "Highlight",
|
"Highlight": "Highlight",
|
||||||
"Pen": "Pen",
|
"Pen": "Pen",
|
||||||
@ -236,4 +240,4 @@
|
|||||||
"Allow once (risky)": "Allow once (risky)",
|
"Allow once (risky)": "Allow once (risky)",
|
||||||
"Deny": "Deny",
|
"Deny": "Deny",
|
||||||
"Invalid security certificate": "has an invalid security certificate."
|
"Invalid security certificate": "has an invalid security certificate."
|
||||||
}
|
}
|
||||||
|
@ -160,7 +160,11 @@
|
|||||||
"You are sharing your screen on {appName}": "You are sharing your screen on {appName}"
|
"You are sharing your screen on {appName}": "You are sharing your screen on {appName}"
|
||||||
},
|
},
|
||||||
"ScreenSnippet": {
|
"ScreenSnippet": {
|
||||||
"Done": "Done",
|
"Add to chat": "Add to chat",
|
||||||
|
"Open menu": "Open menu",
|
||||||
|
"Copy to clipboard": "Copy to clipboard",
|
||||||
|
"Save as": "Save as",
|
||||||
|
"Close": "Close",
|
||||||
"Erase": "Erase",
|
"Erase": "Erase",
|
||||||
"Highlight": "Highlight",
|
"Highlight": "Highlight",
|
||||||
"Pen": "Pen",
|
"Pen": "Pen",
|
||||||
@ -236,4 +240,4 @@
|
|||||||
"Allow once (risky)": "Allow once (risky)",
|
"Allow once (risky)": "Allow once (risky)",
|
||||||
"Deny": "Deny",
|
"Deny": "Deny",
|
||||||
"Invalid security certificate": "has an invalid security certificate."
|
"Invalid security certificate": "has an invalid security certificate."
|
||||||
}
|
}
|
||||||
|
3
src/renderer/assets/single-chevron-down.svg
Normal file
3
src/renderer/assets/single-chevron-down.svg
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<svg width="10" height="7" viewBox="0 0 10 7" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M0.292893 0.792893C0.683417 0.402369 1.31658 0.402369 1.70711 0.792893L5 4.08579L8.29289 0.792893C8.68342 0.402369 9.31658 0.402369 9.70711 0.792893C10.0976 1.18342 10.0976 1.81658 9.70711 2.20711L5.70711 6.20711C5.31658 6.59763 4.68342 6.59763 4.29289 6.20711L0.292893 2.20711C-0.0976311 1.81658 -0.0976311 1.18342 0.292893 0.792893Z" fill="white"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 461 B |
120
src/renderer/components/menu-button.tsx
Normal file
120
src/renderer/components/menu-button.tsx
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
// import { ipcRenderer } from 'electron';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { IListItem } from '../../app/screen-snippet-handler';
|
||||||
|
import { i18n } from '../../common/i18n-preload';
|
||||||
|
|
||||||
|
const { useState, useEffect, useRef } = React;
|
||||||
|
|
||||||
|
interface IMenuButtonProps {
|
||||||
|
listItems: IListItem[];
|
||||||
|
id;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MenuButton: React.FunctionComponent<IMenuButtonProps> = ({
|
||||||
|
listItems,
|
||||||
|
id,
|
||||||
|
}) => {
|
||||||
|
//#region State
|
||||||
|
const [isDisplay, setDisplay] = useState(false);
|
||||||
|
const listRef = useRef<HTMLUListElement>(document.createElement('ul'));
|
||||||
|
const menuButtonRef = useRef<HTMLButtonElement>(
|
||||||
|
document.createElement('button'),
|
||||||
|
);
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region Variables
|
||||||
|
const testId = {
|
||||||
|
menu: `${id}_MENU_BUTTON`,
|
||||||
|
};
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region Handlers
|
||||||
|
const onClickMenuButton = () => {
|
||||||
|
setDisplay(!isDisplay);
|
||||||
|
};
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
//#region UseEffect
|
||||||
|
useEffect(() => {
|
||||||
|
const isContainListElement = (ev) =>
|
||||||
|
listRef.current && listRef.current.contains(ev.target as HTMLElement);
|
||||||
|
const isContainMenuBtnElement = (ev) =>
|
||||||
|
menuButtonRef.current &&
|
||||||
|
menuButtonRef.current.contains(ev.target as HTMLElement);
|
||||||
|
window.addEventListener('click', (ev: MouseEvent) => {
|
||||||
|
if (!isContainListElement(ev) && !isContainMenuBtnElement(ev)) {
|
||||||
|
setDisplay(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.addEventListener('keydown', (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
setDisplay(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('click', () => {
|
||||||
|
if (!isContainListElement || !isContainMenuBtnElement) {
|
||||||
|
setDisplay(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.addEventListener('keydown', (event: KeyboardEvent) => {
|
||||||
|
if (event.key === 'Escape') {
|
||||||
|
setDisplay(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
});
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
const renderListItems = () => {
|
||||||
|
return listItems.map((listItem) => {
|
||||||
|
const sendClick = async () => {
|
||||||
|
await listItem.onClick(listItem.event);
|
||||||
|
setDisplay(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
data-testid={`${id}_${listItem.dataTestId}`}
|
||||||
|
className='list-item general-font'
|
||||||
|
lang={i18n.getLocale()}
|
||||||
|
onClick={sendClick}
|
||||||
|
key={listItem.event}
|
||||||
|
>
|
||||||
|
{listItem.name}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const focusCls = isDisplay ? 'menu-button-focus' : '';
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className='menu-button-wrapper'>
|
||||||
|
<button
|
||||||
|
className={`menu-button ${focusCls}`}
|
||||||
|
onClick={onClickMenuButton}
|
||||||
|
data-testid={testId.menu}
|
||||||
|
ref={menuButtonRef}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src={`../renderer/assets/single-chevron-down.svg`}
|
||||||
|
title={i18n.t('Open menu')()}
|
||||||
|
alt={i18n.t('Open menu')()}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
{isDisplay && (
|
||||||
|
<ul className='menu' ref={listRef}>
|
||||||
|
{renderListItems()}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MenuButton;
|
@ -2,12 +2,14 @@ import { ipcRenderer } from 'electron';
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import { svgAsPngUri } from 'save-svg-as-png';
|
import { svgAsPngUri } from 'save-svg-as-png';
|
||||||
import { i18n } from '../../common/i18n-preload';
|
import { i18n } from '../../common/i18n-preload';
|
||||||
|
import { ScreenShotAnnotation } from '../../common/ipcEvent';
|
||||||
import {
|
import {
|
||||||
AnalyticsElements,
|
AnalyticsElements,
|
||||||
ScreenSnippetActionTypes,
|
ScreenSnippetActionTypes,
|
||||||
} from './../../app/analytics-handler';
|
} from './../../app/analytics-handler';
|
||||||
import AnnotateArea from './annotate-area';
|
import AnnotateArea from './annotate-area';
|
||||||
import ColorPickerPill, { IColor } from './color-picker-pill';
|
import ColorPickerPill, { IColor } from './color-picker-pill';
|
||||||
|
import MenuButton from './menu-button';
|
||||||
|
|
||||||
const { useState, useRef, useEffect, useLayoutEffect } = React;
|
const { useState, useRef, useEffect, useLayoutEffect } = React;
|
||||||
|
|
||||||
@ -92,6 +94,42 @@ const SnippingTool: React.FunctionComponent<ISnippingToolProps> = ({
|
|||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const mergeImage = async () => {
|
||||||
|
const svg = document.getElementById('annotate-area');
|
||||||
|
const mergedImageData = svg ? await svgAsPngUri(svg, {}) : 'MERGE_FAIL';
|
||||||
|
|
||||||
|
return mergedImageData;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onCopyToClipboard = async (eventName) => {
|
||||||
|
const img = await mergeImage();
|
||||||
|
ipcRenderer.send(eventName, {
|
||||||
|
clipboard: img,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSaveAs = async (eventName) => {
|
||||||
|
const img = await mergeImage();
|
||||||
|
ipcRenderer.send(eventName, {
|
||||||
|
clipboard: img,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const menuItem = [
|
||||||
|
{
|
||||||
|
name: i18n.t('Copy to clipboard', SNIPPING_TOOL_NAMESPACE)(),
|
||||||
|
event: ScreenShotAnnotation.COPY_TO_CLIPBOARD,
|
||||||
|
onClick: onCopyToClipboard,
|
||||||
|
dataTestId: 'COPY_TO_CLIPBOARD',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: i18n.t('Save as', SNIPPING_TOOL_NAMESPACE)(),
|
||||||
|
event: ScreenShotAnnotation.SAVE_AS,
|
||||||
|
onClick: onSaveAs,
|
||||||
|
dataTestId: 'SAVE_AS',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const getSnipImageData = (
|
const getSnipImageData = (
|
||||||
{},
|
{},
|
||||||
{
|
{
|
||||||
@ -225,7 +263,7 @@ const SnippingTool: React.FunctionComponent<ISnippingToolProps> = ({
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const done = async () => {
|
const addToChat = async () => {
|
||||||
const svg = document.getElementById('annotate-area');
|
const svg = document.getElementById('annotate-area');
|
||||||
const mergedImageData = svg
|
const mergedImageData = svg
|
||||||
? await svgAsPngUri(document.getElementById('annotate-area'), {})
|
? await svgAsPngUri(document.getElementById('annotate-area'), {})
|
||||||
@ -237,6 +275,10 @@ const SnippingTool: React.FunctionComponent<ISnippingToolProps> = ({
|
|||||||
ipcRenderer.send('upload-snippet', { screenSnippetPath, mergedImageData });
|
ipcRenderer.send('upload-snippet', { screenSnippetPath, mergedImageData });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const onClose = async () => {
|
||||||
|
ipcRenderer.send(ScreenShotAnnotation.CLOSE);
|
||||||
|
};
|
||||||
|
|
||||||
// Removes focus styling from buttons when mouse is clicked
|
// Removes focus styling from buttons when mouse is clicked
|
||||||
document.body.addEventListener('mousedown', () => {
|
document.body.addEventListener('mousedown', () => {
|
||||||
document.body.classList.add('using-mouse');
|
document.body.classList.add('using-mouse');
|
||||||
@ -350,12 +392,20 @@ const SnippingTool: React.FunctionComponent<ISnippingToolProps> = ({
|
|||||||
</main>
|
</main>
|
||||||
<footer>
|
<footer>
|
||||||
<button
|
<button
|
||||||
data-testid='done-button'
|
data-testid='close-button'
|
||||||
className='done-button'
|
className='close-button'
|
||||||
onClick={done}
|
onClick={onClose}
|
||||||
>
|
>
|
||||||
{i18n.t('Done', SNIPPING_TOOL_NAMESPACE)()}
|
{i18n.t('Close', SNIPPING_TOOL_NAMESPACE)()}
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
data-testid='done-button'
|
||||||
|
className='add-to-chat-button'
|
||||||
|
onClick={addToChat}
|
||||||
|
>
|
||||||
|
{i18n.t('Add to chat', SNIPPING_TOOL_NAMESPACE)()}
|
||||||
|
</button>
|
||||||
|
<MenuButton id='snipping-tool' listItems={menuItem}></MenuButton>
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -124,18 +124,42 @@ body.using-mouse :focus {
|
|||||||
padding-top: 16px;
|
padding-top: 16px;
|
||||||
padding-bottom: 24px;
|
padding-bottom: 24px;
|
||||||
|
|
||||||
.done-button {
|
.add-to-chat-button {
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 16px;
|
border-radius: 16px;
|
||||||
font-size: 0.75rem;
|
font-size: 0.75rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
background-color: #008eff;
|
background-color: @electricity-ui-50;
|
||||||
color: white;
|
color: @vanilla-white;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
margin-right: 32px;
|
border-radius: 16px 0px 0px 16px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
width: 80px;
|
width: 102px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.add-to-chat-button:hover {
|
||||||
|
background-color: @electricity-ui-60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-button {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
padding: 8px 16px;
|
||||||
|
width: 67px;
|
||||||
|
height: 32px;
|
||||||
|
border-radius: 16px;
|
||||||
|
font-family: @font-family;
|
||||||
|
color: @graphite-50;
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-right: 16px;
|
||||||
|
border: none;
|
||||||
|
background-color: transparent;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -172,3 +196,73 @@ body.using-mouse :focus {
|
|||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.menu-button {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 8px 8px 8px 6px;
|
||||||
|
width: 30px;
|
||||||
|
height: 32px;
|
||||||
|
background: @electricity-ui-50;
|
||||||
|
border-radius: 0px 16px 16px 0px;
|
||||||
|
margin-left: 1px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-button:focus {
|
||||||
|
background: @electricity-ui-60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-button-focus {
|
||||||
|
background: @electricity-ui-60;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chevron-down {
|
||||||
|
color: @text-color-primary;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu {
|
||||||
|
box-sizing: border-box;
|
||||||
|
top: -85px;
|
||||||
|
left: -113px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 14px 16px;
|
||||||
|
position: absolute;
|
||||||
|
width: 143px;
|
||||||
|
min-height: 80px;
|
||||||
|
background: @vanilla-white;
|
||||||
|
box-shadow: 0px 2px 4px rgba(5, 6, 6, 0.08), 0px 12px 28px rgba(5, 6, 6, 0.16);
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 0px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-button-wrapper {
|
||||||
|
position: relative;
|
||||||
|
margin-right: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.general-font {
|
||||||
|
font-weight: 400;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 20px;
|
||||||
|
color: @graphite-80;
|
||||||
|
font-family: @font-family;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
list-style-type: none;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.general-font:lang(ja-JP) {
|
||||||
|
font-family: @font-family-ja;
|
||||||
|
}
|
||||||
|
@ -9,9 +9,14 @@
|
|||||||
@electricity-ui-05: #e9f2f9;
|
@electricity-ui-05: #e9f2f9;
|
||||||
@electricity-ui-50: #0277d6;
|
@electricity-ui-50: #0277d6;
|
||||||
@electricity-ui-30: #6eb9fd;
|
@electricity-ui-30: #6eb9fd;
|
||||||
|
@electricity-ui-60: #27588e;
|
||||||
|
|
||||||
@graphite-20: #cdcfd4;
|
@graphite-20: #cdcfd4;
|
||||||
@graphite-05: #f1f1f3;
|
@graphite-05: #f1f1f3;
|
||||||
@graphite-80: #27292c;
|
@graphite-80: #27292c;
|
||||||
@graphite-30: #b0b3ba;
|
@graphite-30: #b0b3ba;
|
||||||
@graphite-40: #8f959e;
|
@graphite-40: #8f959e;
|
||||||
@graphite-90: #141618;
|
@graphite-90: #141618;
|
||||||
|
@graphite-50: #717681;
|
||||||
|
|
||||||
|
@vanilla-white: #fff;
|
||||||
|
Loading…
Reference in New Issue
Block a user