app/src/commons/model/menu-update.ts
crusader 778fe832c7 ing
2018-09-19 00:10:08 +09:00

176 lines
4.6 KiB
TypeScript

import { merge } from '@overflow/core/merge';
import { MenuIDs } from '../type';
import { IAppState } from './app-state';
import { TipState } from './tip';
import { AppMenu, MenuItem } from './app-menu';
export interface IMenuItemState {
readonly enabled?: boolean;
}
/**
* Utility class for coalescing updates to menu items
*/
class MenuStateBuilder {
private readonly _state: Map<MenuIDs, IMenuItemState>;
public constructor(state: Map<MenuIDs, IMenuItemState> = new Map()) {
this._state = state;
}
/**
* Returns an Map where each key is a MenuID and the values
* are IMenuItemState instances containing information about
* whether a particular menu item should be enabled/disabled or
* visible/hidden.
*/
public get state() {
return new Map<MenuIDs, IMenuItemState>(this._state);
}
private updateMenuItem<K extends keyof IMenuItemState>(
id: MenuIDs,
state: Pick<IMenuItemState, K>
) {
const currentState = this._state.get(id) || {};
this._state.set(id, merge(currentState, state));
}
/** Set the state of the given menu item id to enabled */
public enable(id: MenuIDs): this {
this.updateMenuItem(id, { enabled: true });
return this;
}
/** Set the state of the given menu item id to disabled */
public disable(id: MenuIDs): this {
this.updateMenuItem(id, { enabled: false });
return this;
}
/** Set the enabledness of the given menu item id */
public setEnabled(id: MenuIDs, enabled: boolean): this {
this.updateMenuItem(id, { enabled });
return this;
}
/**
* Create a new state builder by merging the current state with the state from
* the other state builder. This will replace values in `this` with values
* from `other`.
*/
public merge(other: MenuStateBuilder): MenuStateBuilder {
const merged = new Map<MenuIDs, IMenuItemState>(this._state);
for (const [key, value] of other._state) {
merged.set(key, value);
}
return new MenuStateBuilder(merged);
}
}
function menuItemStateEqual(state: IMenuItemState, menuItem: MenuItem) {
if (
state.enabled !== undefined &&
menuItem.type !== 'separator' &&
menuItem.enabled !== state.enabled
) {
return false;
}
return true;
}
const allMenuIds: ReadonlyArray<MenuIDs> = [
'save',
'preferences',
'about',
'export-csv',
'print',
'language-english',
'language-korean',
];
function getAllMenusDisabledBuilder(): MenuStateBuilder {
const menuStateBuilder = new MenuStateBuilder();
for (const menuId of allMenuIds) {
menuStateBuilder.disable(menuId);
}
return menuStateBuilder;
}
function getMenuState(state: IAppState): Map<MenuIDs, IMenuItemState> {
if (state.currentPopup) {
return getAllMenusDisabledBuilder().state;
}
return getAllMenusEnabledBuilder()
.merge(getInWelcomeFlowBuilder(state.showWelcomeFlow)).state;
}
function getAllMenusEnabledBuilder(): MenuStateBuilder {
const menuStateBuilder = new MenuStateBuilder();
for (const menuId of allMenuIds) {
menuStateBuilder.enable(menuId);
}
return menuStateBuilder;
}
function getInWelcomeFlowBuilder(inWelcomeFlow: boolean): MenuStateBuilder {
const welcomeScopedIds: ReadonlyArray<MenuIDs> = [
'preferences',
'about',
];
const menuStateBuilder = new MenuStateBuilder();
if (inWelcomeFlow) {
for (const id of welcomeScopedIds) {
menuStateBuilder.disable(id);
}
} else {
for (const id of welcomeScopedIds) {
menuStateBuilder.enable(id);
}
}
return menuStateBuilder;
}
/**
* Update the menu state in the main process.
*
* This function will set the enabledness and visibility of menu items
* in the main process based on the AppState. All changes will be
* batched together into one ipc message.
*/
export function updateMenuState(
state: IAppState,
currentAppMenu: AppMenu | null
) {
const menuState = getMenuState(state);
// Try to avoid updating sending the IPC message at all
// if we have a current app menu that we can compare against.
if (currentAppMenu) {
for (const [id, menuItemState] of menuState.entries()) {
const appMenuItem = currentAppMenu.getItemById(id);
if (appMenuItem && menuItemStateEqual(menuItemState, appMenuItem)) {
menuState.delete(id);
}
}
}
if (menuState.size === 0) {
return;
}
// because we can't send Map over the wire, we need to convert
// the remaining entries into an array that can be serialized
const array = new Array<{ id: MenuIDs; state: IMenuItemState }>();
menuState.forEach((value, key) => array.push({ id: key, state: value }));
// ipcUpdateMenuState(array);
}