Typescript - Optimize window actions

This commit is contained in:
Kiran Niranjan 2018-12-19 13:38:10 +05:30
parent 1054884417
commit 81f0a1460e
10 changed files with 219 additions and 146 deletions

View File

@ -6,8 +6,8 @@ import { logger } from '../common/logger';
import { autoLaunchInstance as autoLaunch } from './auto-launch-controller';
import { config, IConfig } from './config-handler';
import { exportCrashDumps, exportLogs } from './reports';
import { updateAlwaysOnTop } from './window-actions';
import { windowHandler } from './window-handler';
import { updateAlwaysOnTop } from './window-utils';
export const menuSections = {
about: 'about',

View File

@ -4,7 +4,10 @@ import { apiCmds, apiName, IApiArgs } from '../common/api-interface';
import { LocaleType } from '../common/i18n';
import { logger } from '../common/logger';
import { activityDetection } from './activity-detection';
import { config } from './config-handler';
import { checkProtocolAction, setProtocolWindow } from './protocol-handler';
import { screenSnippet } from './screen-snippet';
import { activate, handleKeyPress } from './window-actions';
import { windowHandler } from './window-handler';
import {
isValidWindow,
@ -29,33 +32,30 @@ ipcMain.on(apiName.symphonyApi, (event: Electron.Event, arg: IApiArgs) => {
}
switch (arg.cmd) {
/*case ApiCmds.isOnline:
case apiCmds.isOnline:
if (typeof arg.isOnline === 'boolean') {
windowMgr.setIsOnline(arg.isOnline);
windowHandler.isOnline = arg.isOnline;
}
break;*/
break;
case apiCmds.setBadgeCount:
if (typeof arg.count === 'number') {
showBadgeCount(arg.count);
}
break;
/*case ApiCmds.registerProtocolHandler:
protocolHandler.setProtocolWindow(event.sender);
protocolHandler.checkProtocolAction();
break;*/
case apiCmds.registerProtocolHandler:
setProtocolWindow(event.sender);
checkProtocolAction();
break;
case apiCmds.badgeDataUrl:
if (typeof arg.dataUrl === 'string' && typeof arg.count === 'number') {
setDataUrl(arg.dataUrl, arg.count);
}
break;
/*case ApiCmds.activate:
case apiCmds.activate:
if (typeof arg.windowName === 'string') {
windowMgr.activate(arg.windowName);
activate(arg.windowName);
}
break;
case ApiCmds.registerBoundsChange:
windowMgr.setBoundsChangeWindow(event.sender);
break;*/
case apiCmds.registerLogger:
// renderer window that has a registered logger from JS.
logger.setLoggerWindow(event.sender);
@ -76,12 +76,13 @@ ipcMain.on(apiName.symphonyApi, (event: Electron.Event, arg: IApiArgs) => {
sanitize(arg.windowName);
}
break;
/*case ApiCmds.bringToFront:
case apiCmds.bringToFront:
// validates the user bring to front config and activates the wrapper
if (typeof arg.reason === 'string' && arg.reason === 'notification') {
bringToFront(arg.windowName, arg.reason);
const shouldBringToFront = config.getConfigFields([ 'bringToFront' ]);
if (shouldBringToFront) activate(arg.windowName, false);
}
break;*/
break;
case apiCmds.openScreenPickerWindow:
if (Array.isArray(arg.sources) && typeof arg.id === 'number') {
windowHandler.createScreenPickerWindow(event.sender, arg.sources, arg.id);
@ -114,11 +115,11 @@ ipcMain.on(apiName.symphonyApi, (event: Electron.Event, arg: IApiArgs) => {
updateLocale(arg.locale as LocaleType);
}
break;
/*case ApiCmds.keyPress:
case apiCmds.keyPress:
if (typeof arg.keyCode === 'number') {
windowMgr.handleKeyPress(arg.keyCode);
handleKeyPress(arg.keyCode);
}
break;*/
break;
case apiCmds.openScreenSnippet:
screenSnippet.capture(event.sender);
break;

View File

@ -21,7 +21,7 @@ const setProtocolUrl = (uri: string): void => {
* Processes a protocol uri
* @param {String} uri - the uri opened in the format 'symphony://abc?def=ghi'
*/
const processProtocolUri = (uri: string): void => {
export const processProtocolUri = (uri: string): void => {
logger.info(`Processing protocol action, uri ${uri}`);
if (!protocolWindow) {
@ -41,7 +41,7 @@ const processProtocolUri = (uri: string): void => {
* @param uri
* @param isAppAlreadyOpen {Boolean} whether the app is already open
*/
const handleProtocolAction = (uri: string, isAppAlreadyOpen: boolean): void => {
export const handleProtocolAction = (uri: string, isAppAlreadyOpen: boolean): void => {
if (!isAppAlreadyOpen) {
@ -72,7 +72,7 @@ const handleProtocolAction = (uri: string, isAppAlreadyOpen: boolean): void => {
* @param argv {Array} an array of command line arguments
* @param isAppAlreadyOpen {Boolean} whether the app is already open
*/
const processProtocolArgv = (argv: string[], isAppAlreadyOpen: boolean): void => {
export const processProtocolArgv = (argv: string[], isAppAlreadyOpen: boolean): void => {
// In case of windows, we need to handle protocol handler
// manually because electron doesn't emit
@ -99,7 +99,7 @@ const processProtocolArgv = (argv: string[], isAppAlreadyOpen: boolean): void =>
* Sets the protocol window
* @param {Object} win - the renderer window
*/
const setProtocolWindow = (win: Electron.WebContents): void => {
export const setProtocolWindow = (win: Electron.WebContents): void => {
logger.info(`Setting protocol window ${win}`);
protocolWindow = win;
};
@ -107,7 +107,7 @@ const setProtocolWindow = (win: Electron.WebContents): void => {
/**
* Checks to see if the app was opened by a uri
*/
const checkProtocolAction = (): void => {
export const checkProtocolAction = (): void => {
logger.info('Checking if we have a cached protocol url');
if (protocolUrl) {
logger.info(`Found a cached protocol url (${protocolUrl}), processing it`);
@ -121,9 +121,7 @@ const checkProtocolAction = (): void => {
* Gets the protocol url set against an instance
* @returns {*}
*/
const getProtocolUrl = (): string | undefined => {
export const getProtocolUrl = (): string | undefined => {
logger.info(`Getting the property protocol url ${protocolUrl}`);
return protocolUrl;
};
export {processProtocolUri, processProtocolArgv, setProtocolWindow, checkProtocolAction, getProtocolUrl};
};

View File

@ -9,8 +9,8 @@ import { IScreenSnippet } from '../common/api-interface';
import { isDevEnv, isMac } from '../common/env';
import { i18n } from '../common/i18n';
import { logger } from '../common/logger';
import { updateAlwaysOnTop } from './window-actions';
import { windowHandler } from './window-handler';
import { updateAlwaysOnTop } from './window-utils';
const readFile = util.promisify(fs.readFile);

View File

@ -1,8 +1,11 @@
import { BrowserWindow } from 'electron';
import { IBoundsChange, KeyCodes } from '../common/api-interface';
import { isWindowsOS } from '../common/env';
import { throttle } from '../common/utils';
import { config } from './config-handler';
import { ICustomBrowserWindow } from './window-handler';
import { ICustomBrowserWindow, windowHandler } from './window-handler';
import { showPopupMenu } from './window-utils';
export const saveWindowSettings = (): void => {
const browserWindow = BrowserWindow.getFocusedWindow() as ICustomBrowserWindow;
@ -11,7 +14,7 @@ export const saveWindowSettings = (): void => {
const [ x, y ] = browserWindow.getPosition();
const [ width, height ] = browserWindow.getSize();
if (x && y && width && height) {
browserWindow.webContents.send('boundChanges', { x, y, width, height });
browserWindow.webContents.send('boundChanges', { x, y, width, height, windowName: browserWindow.winName } as IBoundsChange);
if (browserWindow.winName === 'main') {
const isMaximized = browserWindow.isMaximized();
@ -37,4 +40,86 @@ export const leaveFullScreen = () => {
}
};
export const throttledWindowChanges = throttle(saveWindowSettings, 1000);
export const throttledWindowChanges = throttle(saveWindowSettings, 1000);
/**
* Tries finding a window we have created with given name. If found, then
* brings to front and gives focus.
*
* @param {string} windowName Name of target window. Note: main window has
* name 'main'.
* @param {Boolean} shouldFocus whether to get window to focus or just show
* without giving focus
*/
export const activate = (windowName: string, shouldFocus: boolean = true): void => {
// Electron-136: don't activate when the app is reloaded programmatically
if (windowHandler.isAutoReload) return;
const windows = windowHandler.getAllWindows();
for (const key in windows) {
if (windows.hasOwnProperty(key)) {
const window = windows[ key ];
if (window && !window.isDestroyed() && window.winName === windowName) {
// Bring the window to the top without focusing
// Flash task bar icon in Windows for windows
if (!shouldFocus) {
window.moveTop();
return isWindowsOS ? window.flashFrame(true) : null;
}
return window.isMinimized() ? window.restore() : window.show();
}
}
}
};
/**
* Sets always on top property based on isAlwaysOnTop
*
* @param shouldSetAlwaysOnTop
* @param shouldActivateMainWindow
*/
export const updateAlwaysOnTop = (shouldSetAlwaysOnTop: boolean, shouldActivateMainWindow: boolean = true): void => {
const browserWins: ICustomBrowserWindow[] = BrowserWindow.getAllWindows() as ICustomBrowserWindow[];
if (browserWins.length > 0) {
browserWins
.filter((browser) => typeof browser.notificationObj !== 'object')
.forEach((browser) => browser.setAlwaysOnTop(shouldSetAlwaysOnTop));
// An issue where changing the alwaysOnTop property
// focus the pop-out window
// Issue - Electron-209/470
const mainWindow = windowHandler.getMainWindow();
if (mainWindow && mainWindow.winName && shouldActivateMainWindow) {
activate(mainWindow.winName);
}
}
};
/**
* Method that handles key press
*
* @param key {number}
*/
export const handleKeyPress = (key: number): void => {
switch (key) {
case KeyCodes.Esc: {
const focusedWindow = BrowserWindow.getFocusedWindow();
if (focusedWindow && !focusedWindow.isDestroyed() && focusedWindow.isFullScreen()) {
focusedWindow.setFullScreen(false);
}
break;
}
case KeyCodes.Alt:
const browserWin = BrowserWindow.getFocusedWindow();
if (browserWin && !browserWin.isDestroyed()) {
showPopupMenu({ window: browserWin });
}
break;
default:
break;
}
};

View File

@ -86,6 +86,7 @@ export class WindowHandler {
public appMenu: AppMenu | null;
public isAutoReload: boolean;
public isOnline: boolean;
private readonly windowOpts: ICustomBrowserWindowConstructorOpts;
private readonly globalConfig: IConfig;
@ -108,6 +109,7 @@ export class WindowHandler {
this.windows = {};
this.windowOpts = { ...this.getMainWindowOpts(), ...opts };
this.isAutoReload = false;
this.isOnline = true;
this.isCustomTitleBarAndWindowOS = isWindowsOS && this.config.isCustomTitleBar;
this.appMenu = null;

View File

@ -2,11 +2,11 @@ import { app, BrowserWindow, nativeImage } from 'electron';
import * as path from 'path';
import * as url from 'url';
import { isMac, isWindowsOS } from '../common/env';
import { isMac } from '../common/env';
import { i18n, LocaleType } from '../common/i18n';
import { logger } from '../common/logger';
import { screenSnippet } from './screen-snippet';
import { ICustomBrowserWindow, windowHandler } from './window-handler';
import { windowHandler } from './window-handler';
const checkValidWindow = true;
@ -116,62 +116,6 @@ export const setDataUrl = (dataUrl: string, count: number): void => {
}
};
/**
* Tries finding a window we have created with given name. If found, then
* brings to front and gives focus.
*
* @param {string} windowName Name of target window. Note: main window has
* name 'main'.
* @param {Boolean} shouldFocus whether to get window to focus or just show
* without giving focus
*/
export const activate = (windowName: string, shouldFocus: boolean = true): void => {
// Electron-136: don't activate when the app is reloaded programmatically
if (windowHandler.isAutoReload) return;
const windows = windowHandler.getAllWindows();
for (const key in windows) {
if (windows.hasOwnProperty(key)) {
const window = windows[ key ];
if (window && !window.isDestroyed() && window.winName === windowName) {
// Bring the window to the top without focusing
// Flash task bar icon in Windows for windows
if (!shouldFocus) {
window.moveTop();
return isWindowsOS ? window.flashFrame(true) : null;
}
return window.isMinimized() ? window.restore() : window.show();
}
}
}
};
/**
* Sets always on top property based on isAlwaysOnTop
*
* @param shouldSetAlwaysOnTop
* @param shouldActivateMainWindow
*/
export const updateAlwaysOnTop = (shouldSetAlwaysOnTop: boolean, shouldActivateMainWindow: boolean = true): void => {
const browserWins: ICustomBrowserWindow[] = BrowserWindow.getAllWindows() as ICustomBrowserWindow[];
if (browserWins.length > 0) {
browserWins
.filter((browser) => typeof browser.notificationObj !== 'object')
.forEach((browser) => browser.setAlwaysOnTop(shouldSetAlwaysOnTop));
// An issue where changing the alwaysOnTop property
// focus the pop-out window
// Issue - Electron-209/470
const mainWindow = windowHandler.getMainWindow();
if (mainWindow && mainWindow.winName && shouldActivateMainWindow) {
activate(mainWindow.winName);
}
}
};
/**
* Ensure events comes from a window that we have created.
*

View File

@ -63,4 +63,13 @@ export interface IScreenSnippet {
data?: string;
message?: string;
type: ScreenSnippetDataType;
}
export interface IBoundsChange extends Electron.Rectangle {
windowName: string;
}
export enum KeyCodes {
Esc = 27,
Alt = 18,
}

View File

@ -3,7 +3,6 @@ import * as React from 'react';
import { apiCmds, apiName } from '../../common/api-interface';
import { i18n } from '../../common/i18n';
import { throttle } from '../../common/utils';
interface IState {
isMaximized: boolean;
@ -11,11 +10,6 @@ interface IState {
titleBarHeight: string;
}
const enum KeyCodes {
Esc = 27,
Alt = 18,
}
export default class WindowsTitleBar extends React.Component<{}, IState> {
private readonly window: Electron.BrowserWindow;
private readonly eventHandlers = {
@ -25,14 +19,10 @@ export default class WindowsTitleBar extends React.Component<{}, IState> {
onShowMenu: () => this.showMenu(),
onUnmaximize: () => this.unmaximize(),
};
private isAltKey: boolean;
private isMenuOpen: boolean;
constructor(props) {
super(props);
this.window = remote.getCurrentWindow();
this.isAltKey = false;
this.isMenuOpen = false;
this.state = {
isFullScreen: this.window.isFullScreen(),
isMaximized: this.window.isMaximized(),
@ -47,32 +37,6 @@ export default class WindowsTitleBar extends React.Component<{}, IState> {
this.window.on('unmaximize', () => this.updateState({ isMaximized: false }));
this.window.on('enter-full-screen', () => this.updateState({ isFullScreen: true }));
this.window.on('leave-full-screen', () => this.updateState({ isFullScreen: false }));
// Handle key down events
const throttledKeyDown = throttle( (event) => {
this.isAltKey = event.keyCode === KeyCodes.Alt;
}, 500);
// Handle key up events
const throttledKeyUp = throttle( (event) => {
if (this.isAltKey && (event.keyCode === KeyCodes.Alt || KeyCodes.Esc)) {
this.isMenuOpen = !this.isMenuOpen;
}
if (this.isAltKey && this.isMenuOpen && event.keyCode === KeyCodes.Alt) {
this.showMenu();
}
}, 500);
// Handle mouse down event
const throttleMouseDown = throttle(() => {
if (this.isAltKey && this.isMenuOpen) {
this.isMenuOpen = !this.isMenuOpen;
}
}, 500);
window.addEventListener('keyup', throttledKeyUp, true);
window.addEventListener('keydown', throttledKeyDown, true);
window.addEventListener('mousedown', throttleMouseDown, { capture: true });
}
public componentDidMount() {

View File

@ -6,16 +6,21 @@ import {
apiName,
IActivityDetection,
IBadgeCount,
IScreenSnippet,
IBoundsChange,
IScreenSnippet, KeyCodes,
} from '../common/api-interface';
import { i18n, LocaleType } from '../common/i18n';
import { throttle } from '../common/utils';
import { getSource } from './desktop-capturer';
let isAltKey = false;
let isMenuOpen = false;
interface ILocalObject {
ipcRenderer;
activityDetection?: (arg: IActivityDetection) => void;
screenSnippet?: (arg: IScreenSnippet) => void;
activityDetectionCallback?: (arg: IActivityDetection) => void;
screenSnippetCallback?: (arg: IScreenSnippet) => void;
boundsChangeCallback?: (arg: IBoundsChange) => void;
}
const local: ILocalObject = {
@ -68,12 +73,12 @@ export class SSFApi {
* Allows JS to register a activity detector that can be used by electron main process.
*
* @param {Object} period - minimum user idle time in millisecond
* @param {Object} activityDetection - function that can be called accepting
* @param {Object} activityDetectionCallback - function that can be called accepting
* @example registerActivityDetection(40000, func)
*/
public registerActivityDetection(period: number, activityDetection: Partial<ILocalObject>): void {
if (typeof activityDetection === 'function') {
local.activityDetection = activityDetection;
public registerActivityDetection(period: number, activityDetectionCallback: Partial<ILocalObject>): void {
if (typeof activityDetectionCallback === 'function') {
local.activityDetectionCallback = activityDetectionCallback;
// only main window can register
local.ipcRenderer.send(apiName.symphonyApi, {
@ -83,14 +88,27 @@ export class SSFApi {
}
}
/**
* Allows JS to register a callback to be invoked when size/positions
* changes for any pop-out window (i.e., window.open). The main
* process will emit IPC event 'boundsChange' (see below). Currently
* only one window can register for bounds change.
* @param {Function} callback Function invoked when bounds changes.
*/
public registerBoundsChange(callback: () => void): void {
if (typeof callback === 'function') {
local.boundsChangeCallback = callback;
}
}
/**
* Allow user to capture portion of screen
*
* @param screenSnippet {function}
* @param screenSnippetCallback {function}
*/
public openScreenSnippet(screenSnippet: Partial<IScreenSnippet>): void {
if (typeof screenSnippet === 'function') {
local.screenSnippet = screenSnippet;
public openScreenSnippet(screenSnippetCallback: Partial<IScreenSnippet>): void {
if (typeof screenSnippetCallback === 'function') {
local.screenSnippetCallback = screenSnippetCallback;
local.ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.openScreenSnippet,
@ -130,7 +148,7 @@ export class SSFApi {
*/
// Creates a data url
ipcRenderer.on('create-badge-data-url', (_event: Event, arg: IBadgeCount) => {
local.ipcRenderer.on('create-badge-data-url', (_event: Event, arg: IBadgeCount) => {
const count = arg && arg.count || 0;
// create 32 x 32 img
@ -169,28 +187,80 @@ ipcRenderer.on('create-badge-data-url', (_event: Event, arg: IBadgeCount) => {
}
});
ipcRenderer.on('screen-snippet-data', (_event: Event, arg: IScreenSnippet) => {
if (typeof arg === 'object' && typeof local.screenSnippet === 'function') {
local.screenSnippet(arg);
local.ipcRenderer.on('screen-snippet-data', (_event: Event, arg: IScreenSnippet) => {
if (typeof arg === 'object' && typeof local.screenSnippetCallback === 'function') {
local.screenSnippetCallback(arg);
}
});
ipcRenderer.on('activity', (_event: Event, arg: IActivityDetection) => {
if (typeof arg === 'object' && typeof local.activityDetection === 'function') {
local.activityDetection(arg);
local.ipcRenderer.on('activity', (_event: Event, arg: IActivityDetection) => {
if (typeof arg === 'object' && typeof local.activityDetectionCallback === 'function') {
local.activityDetectionCallback(arg);
}
});
// listen for notifications that some window size/position has changed
local.ipcRenderer.on('boundsChange', (_event, arg: IBoundsChange): void => {
const { x, y, height, width, windowName } = arg;
if (x && y && height && width && windowName && typeof local.boundsChangeCallback === 'function') {
local.boundsChangeCallback({
x,
y,
height,
width,
windowName,
});
}
});
// Invoked whenever the app is reloaded/navigated
const sanitize = (): void => {
local.ipcRenderer.send(apiName, {
local.ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.sanitize,
windowName: window.name || 'main',
});
};
// listens for the online/offline events and updates the main process
const updateOnlineStatus = (): void => {
local.ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.isOnline,
isOnline: window.navigator.onLine,
});
};
// Handle key down events
const throttledKeyDown = throttle( (event) => {
isAltKey = event.keyCode === KeyCodes.Alt;
}, 500);
// Handle key up events
const throttledKeyUp = throttle( (event) => {
if (isAltKey && (event.keyCode === KeyCodes.Alt || KeyCodes.Esc)) {
isMenuOpen = !isMenuOpen;
}
if (isAltKey && isMenuOpen && event.keyCode === KeyCodes.Alt) {
local.ipcRenderer.send(apiName.symphonyApi, {
cmd: apiCmds.keyPress,
keyCode: event.keyCode,
});
}
}, 500);
// Handle mouse down event
const throttleMouseDown = throttle(() => {
if (isAltKey && isMenuOpen) {
isMenuOpen = !isMenuOpen;
}
}, 500);
/**
* Window Events
*/
window.addEventListener('beforeunload', sanitize, false);
window.addEventListener('beforeunload', sanitize, false);
window.addEventListener('offline', updateOnlineStatus, false);
window.addEventListener('online', updateOnlineStatus, false);
window.addEventListener('keyup', throttledKeyUp, true);
window.addEventListener('keydown', throttledKeyDown, true);
window.addEventListener('mousedown', throttleMouseDown, { capture: true });