refactor: move basic menu template to each services

This commit is contained in:
tiddlygit-test 2021-01-17 17:19:19 +08:00
parent b7fca74956
commit f5f4091f8d
11 changed files with 349 additions and 396 deletions

View file

@ -4,6 +4,7 @@
"fullscreenable",
"maximizable",
"minimizable",
"submenu",
"subwiki",
"subwiki's",
"tiddlywiki's"

View file

@ -6,7 +6,7 @@ import isDev from 'electron-is-dev';
import settings from 'electron-settings';
import { autoUpdater } from 'electron-updater';
import { clearMainBindings } from '@services/libs/i18n/i18next-electron-fs-backend';
import { clearMainBindings, buildLanguageMenu } from '@services/libs/i18n/i18next-electron-fs-backend';
import { ThemeChannel } from '@/constants/channels';
import { container } from '@services/container';
import { logger } from '@services/libs/log';
@ -53,6 +53,7 @@ const viewService = container.resolve(View);
const wikiService = container.resolve(Wiki);
const windowService = container.resolve(Window);
const workspaceService = container.resolve(Workspace);
const workspaceViewService = container.resolve(WorkspaceView);
app.on('second-instance', () => {
// Someone tried to run a second instance, we should focus our window.
@ -139,8 +140,8 @@ if (!gotTheLock) {
proxyBypassRules,
});
}
// apply theme
nativeTheme.themeSource = themeSource;
menuService.buildMenu();
nativeTheme.addListener('updated', () => {
windowService.sendToAllWindows(ThemeChannel.nativeThemeUpdated);
viewService.reloadViewsDarkReader();
@ -200,10 +201,10 @@ if (!gotTheLock) {
// getContentSize is not updated immediately
// try once after 0.2s (for fast computer), another one after 1s (to be sure)
setTimeout(() => {
ipcMain.emit('request-realign-active-workspace');
workspaceViewService.realignActiveWorkspace();
}, 200);
setTimeout(() => {
ipcMain.emit('request-realign-active-workspace');
workspaceViewService.realignActiveWorkspace();
}, 1000);
};
mainWindow.on('maximize', handleMaximize);
@ -215,6 +216,11 @@ if (!gotTheLock) {
.then(() => {
// trigger whenTrulyReady
ipcMain.emit(customCommonInitFinishedEvent);
})
.then(() => {
// build menu at last, this is not noticeable to user, so do it last
buildLanguageMenu();
menuService.buildMenu();
});
};
app.on('ready', () => {

View file

@ -79,151 +79,6 @@ function createMenu() {
},
{
label: 'View',
submenu: [
{
label: global.sidebar ? 'Hide Sidebar' : 'Show Sidebar',
accelerator: 'CmdOrCtrl+Alt+S',
click: () => {
ipcMain.emit('request-set-preference', null, 'sidebar', !global.sidebar);
ipcMain.emit('request-realign-active-workspace');
},
},
{
label: global.navigationBar ? 'Hide Navigation Bar' : 'Show Navigation Bar',
accelerator: 'CmdOrCtrl+Alt+N',
click: () => {
ipcMain.emit('request-set-preference', null, 'navigationBar', !global.navigationBar);
ipcMain.emit('request-realign-active-workspace');
},
},
{
label: global.titleBar ? 'Hide Title Bar' : 'Show Title Bar',
accelerator: 'CmdOrCtrl+Alt+T',
enabled: process.platform === 'darwin',
visible: process.platform === 'darwin',
click: () => {
ipcMain.emit('request-set-preference', null, 'titleBar', !global.titleBar);
ipcMain.emit('request-realign-active-workspace');
},
},
// same behavior as BrowserWindow with autoHideMenuBar: true
// but with addition to readjust BrowserView so it won't cover the menu bar
{
label: 'Toggle Menu Bar',
visible: false,
accelerator: 'Alt+M',
enabled: process.platform === 'win32',
click: (menuItem: any, browserWindow: any) => {
// if back is called in popup window
// open menu bar in the popup window instead
if (browserWindow && browserWindow.isPopup) {
browserWindow.setMenuBarVisibility(!browserWindow.isMenuBarVisible());
return;
}
const win = mainWindow.get();
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
win.setMenuBarVisibility(!win.isMenuBarVisible());
ipcMain.emit('request-realign-active-workspace');
},
},
{ type: 'separator' },
{ role: 'togglefullscreen' },
{
label: 'Actual Size',
accelerator: 'CmdOrCtrl+0',
click: (menuItem: any, browserWindow: any) => {
// if item is called in popup window
// open menu bar in the popup window instead
if (browserWindow && browserWindow.isPopup) {
const contents = browserWindow.webContents;
contents.zoomFactor = 1;
return;
}
const win = mainWindow.get();
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
if (win !== null && win.getBrowserView() !== null) {
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
const contents = win.getBrowserView().webContents;
contents.zoomFactor = 1;
}
},
enabled: hasWorkspaces,
},
{
label: 'Zoom In',
accelerator: 'CmdOrCtrl+=',
click: (menuItem: any, browserWindow: any) => {
// if item is called in popup window
// open menu bar in the popup window instead
if (browserWindow && browserWindow.isPopup) {
const contents = browserWindow.webContents;
contents.zoomFactor += 0.1;
return;
}
const win = mainWindow.get();
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
if (win !== null && win.getBrowserView() !== null) {
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
const contents = win.getBrowserView().webContents;
contents.zoomFactor += 0.1;
}
},
enabled: hasWorkspaces,
},
{
label: 'Zoom Out',
accelerator: 'CmdOrCtrl+-',
click: (menuItem: any, browserWindow: any) => {
// if item is called in popup window
// open menu bar in the popup window instead
if (browserWindow && browserWindow.isPopup) {
const contents = browserWindow.webContents;
contents.zoomFactor -= 0.1;
return;
}
const win = mainWindow.get();
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
if (win !== null && win.getBrowserView() !== null) {
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
const contents = win.getBrowserView().webContents;
contents.zoomFactor -= 0.1;
}
},
enabled: hasWorkspaces,
},
{ type: 'separator' },
{
label: 'Reload This Page',
accelerator: 'CmdOrCtrl+R',
click: (menuItem: any, browserWindow: any) => {
// if item is called in popup window
// open menu bar in the popup window instead
if (browserWindow && browserWindow.isPopup) {
browserWindow.webContents.reload();
return;
}
const win = mainWindow.get();
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
if (win !== null && win.getBrowserView() !== null) {
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
win.getBrowserView().webContents.reload();
}
},
enabled: hasWorkspaces,
},
{ type: 'separator' },
{
label: 'Developer Tools',
submenu: [
{
label: 'Open Developer Tools of Active Workspace',
accelerator: 'CmdOrCtrl+Option+I',
click: () => getActiveBrowserView().webContents.openDevTools(),
enabled: hasWorkspaces,
},
],
},
],
},
// language menu
{

View file

@ -6,6 +6,7 @@ import { IpcRenderer, IpcMain, BrowserWindow, IpcMainInvokeEvent, IpcRendererEve
import { Window } from '@services/windows';
import { Preference } from '@services/preferences';
import { View } from '@services/view';
import { MenuService } from '@services/menu';
import { container } from '@services/container';
import { LOCALIZATION_FOLDER } from '@services/constants/paths';
import { I18NChannels } from '@/constants/channels';
@ -105,10 +106,14 @@ const whitelistMap = JSON.parse(fs.readFileSync(path.join(LOCALIZATION_FOLDER, '
const whiteListedLanguages = Object.keys(whitelistMap);
export function getLanguageMenu(): MenuItemConstructorOptions[] {
/**
* Register languages into language menu, call this function after container init
*/
export function buildLanguageMenu(): void {
const preferenceService = container.resolve(Preference);
const windowService = container.resolve(Window);
const viewService = container.resolve(View);
const menuService = container.resolve(MenuService);
const subMenu: MenuItemConstructorOptions[] = [];
for (const language of whiteListedLanguages) {
subMenu.push({
@ -127,5 +132,5 @@ export function getLanguageMenu(): MenuItemConstructorOptions[] {
});
}
return subMenu;
menuService.insertMenu('Language', subMenu);
}

View file

@ -1,29 +1,41 @@
/* eslint-disable global-require */
import Transport from 'winston-transport';
import { container } from '@services/container';
import { View } from '@services/view';
import { Window } from '@services/windows';
import { WindowNames } from '@services/windows/WindowProperties';
const handlers = {
createWikiProgress: (message: any) => {
require('../../windows/add-workspace') // require here to prevent possible circular dependence
.get()
.webContents.send('create-wiki-progress', message);
createWikiProgress: (message: string) => {
const windowService = container.resolve(Window);
const createWorkspaceWindow = windowService.get(WindowNames.addWorkspace);
createWorkspaceWindow?.webContents?.send('create-wiki-progress', message);
},
wikiSyncProgress: (message: any) => {
const { getActiveBrowserView } = require('../views');
const browserView = getActiveBrowserView();
if (browserView) {
browserView.webContents.send('wiki-sync-progress', message);
}
wikiSyncProgress: (message: string) => {
const viewService = container.resolve(View);
const browserView = viewService.getActiveBrowserView();
browserView?.webContents?.send('wiki-sync-progress', message);
},
};
export type IHandlers = typeof handlers;
export interface IInfo {
/** which method or handler function we are logging for */
handler: keyof IHandlers;
/** the detailed massage for debugging */
message: string;
}
export default class RendererTransport extends Transport {
log(info: any, callback: any) {
log(info: IInfo, callback: () => unknown): void {
setImmediate(() => {
this.emit('logged', info);
});
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (info.handler && info.handler in handlers) {
// @ts-expect-error ts-migrate(7053) FIXME: Element implicitly has an 'any' type because expre... Remove this comment to see the full error message
handlers[info.handler](info.message);
}

View file

@ -5,19 +5,43 @@ import serviceIdentifiers from '@services/serviceIdentifier';
import { Preference } from '@services/preferences';
import { View } from '@services/view';
interface DeferredMenuItemConstructorOptions extends Omit<MenuItemConstructorOptions, 'label' | 'enabled' | 'submenu'> {
label?: (() => string) | string;
enabled?: (() => boolean) | boolean;
submenu?:
| (() => Array<MenuItemConstructorOptions | DeferredMenuItemConstructorOptions>)
| Array<MenuItemConstructorOptions | DeferredMenuItemConstructorOptions>;
}
@injectable()
export class MenuService {
private readonly menuTemplate: MenuItemConstructorOptions[];
private readonly menuTemplate: DeferredMenuItemConstructorOptions[];
/**
* Rebuild or create menubar from the latest menu template, will be call after some method change the menuTemplate
* You don't need to call this after calling method like insertMenu, it will be call automatically.
*/
public buildMenu(): void {
const menu = Menu.buildFromTemplate(this.menuTemplate);
const menu = Menu.buildFromTemplate(this.getCurrentMenuItemConstructorOptions(this.menuTemplate));
Menu.setApplicationMenu(menu);
}
private getCurrentMenuItemConstructorOptions(
submenu: Array<DeferredMenuItemConstructorOptions | MenuItemConstructorOptions> = this.menuTemplate,
): MenuItemConstructorOptions[] {
return submenu.map((item) => ({
...item,
label: typeof item.label === 'function' ? item.label() : item.label,
enabled: typeof item.enabled === 'function' ? item.enabled() : item.enabled,
submenu:
typeof item.submenu === 'function'
? this.getCurrentMenuItemConstructorOptions(item.submenu())
: item.submenu instanceof Menu
? item.submenu
: this.getCurrentMenuItemConstructorOptions(item.submenu),
}));
}
constructor(
@inject(serviceIdentifiers.Preference) private readonly preferenceService: Preference,
@inject(serviceIdentifiers.View) private readonly viewService: View,
@ -28,6 +52,7 @@ export class MenuService {
this.menuTemplate = [
{
label: 'Edit',
id: 'Edit',
submenu: [
{ role: 'undo' },
{ role: 'redo' },
@ -35,216 +60,37 @@ export class MenuService {
{ role: 'cut' },
{ role: 'copy' },
{ role: 'paste' },
{ role: 'pasteandmatchstyle' },
{ role: 'pasteAndMatchStyle' },
{ role: 'delete' },
{ role: 'selectall' },
{ role: 'selectAll' },
{ type: 'separator' },
{
label: 'Find',
accelerator: 'CmdOrCtrl+F',
click: () => {
const win = mainWindow.get();
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
if (win !== null && win.getBrowserView() !== null) {
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
win.webContents.focus();
(win as any).send('open-find-in-page');
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
const contentSize = win.getContentSize();
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
const view = win.getBrowserView();
// @ts-expect-error ts-migrate(2531) FIXME: Object is possibly 'null'.
view.setBounds(getViewBounds(contentSize, true));
}
},
enabled: hasWorkspaces,
},
{
label: 'Find Next',
accelerator: 'CmdOrCtrl+G',
click: () => {
const win = mainWindow.get();
(win as any).send('request-back-find-in-page', true);
},
enabled: hasWorkspaces,
},
{
label: 'Find Previous',
accelerator: 'Shift+CmdOrCtrl+G',
click: () => {
const win = mainWindow.get();
(win as any).send('request-back-find-in-page', false);
},
enabled: hasWorkspaces,
},
],
},
{
label: 'View',
submenu: [
{
label: global.sidebar ? 'Hide Sidebar' : 'Show Sidebar',
accelerator: 'CmdOrCtrl+Alt+S',
click: () => {
ipcMain.emit('request-set-preference', null, 'sidebar', !global.sidebar);
ipcMain.emit('request-realign-active-workspace');
},
},
{
label: global.navigationBar ? 'Hide Navigation Bar' : 'Show Navigation Bar',
accelerator: 'CmdOrCtrl+Alt+N',
click: () => {
ipcMain.emit('request-set-preference', null, 'navigationBar', !global.navigationBar);
ipcMain.emit('request-realign-active-workspace');
},
},
{
label: global.titleBar ? 'Hide Title Bar' : 'Show Title Bar',
accelerator: 'CmdOrCtrl+Alt+T',
enabled: process.platform === 'darwin',
visible: process.platform === 'darwin',
click: () => {
ipcMain.emit('request-set-preference', null, 'titleBar', !global.titleBar);
ipcMain.emit('request-realign-active-workspace');
},
},
// same behavior as BrowserWindow with autoHideMenuBar: true
// but with addition to readjust BrowserView so it won't cover the menu bar
{
label: 'Toggle Menu Bar',
visible: false,
accelerator: 'Alt+M',
enabled: process.platform === 'win32',
click: (menuItem, browserWindow) => {
// if back is called in popup window
// open menu bar in the popup window instead
if (browserWindow && browserWindow.isPopup) {
browserWindow.setMenuBarVisibility(!browserWindow.isMenuBarVisible());
return;
}
const win = mainWindow.get();
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
win.setMenuBarVisibility(!win.isMenuBarVisible());
ipcMain.emit('request-realign-active-workspace');
},
},
{ type: 'separator' },
{ role: 'togglefullscreen' },
{
label: 'Actual Size',
accelerator: 'CmdOrCtrl+0',
click: (menuItem, browserWindow) => {
// if item is called in popup window
// open menu bar in the popup window instead
if (browserWindow && browserWindow.isPopup) {
const contents = browserWindow.webContents;
contents.zoomFactor = 1;
return;
}
const win = mainWindow.get();
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
if (win !== null && win.getBrowserView() !== null) {
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
const contents = win.getBrowserView().webContents;
contents.zoomFactor = 1;
}
},
enabled: hasWorkspaces,
},
{
label: 'Zoom In',
accelerator: 'CmdOrCtrl+=',
click: (menuItem, browserWindow) => {
// if item is called in popup window
// open menu bar in the popup window instead
if (browserWindow && browserWindow.isPopup) {
const contents = browserWindow.webContents;
contents.zoomFactor += 0.1;
return;
}
const win = mainWindow.get();
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
if (win !== null && win.getBrowserView() !== null) {
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
const contents = win.getBrowserView().webContents;
contents.zoomFactor += 0.1;
}
},
enabled: hasWorkspaces,
},
{
label: 'Zoom Out',
accelerator: 'CmdOrCtrl+-',
click: (menuItem, browserWindow) => {
// if item is called in popup window
// open menu bar in the popup window instead
if (browserWindow && browserWindow.isPopup) {
const contents = browserWindow.webContents;
contents.zoomFactor -= 0.1;
return;
}
const win = mainWindow.get();
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
if (win !== null && win.getBrowserView() !== null) {
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
const contents = win.getBrowserView().webContents;
contents.zoomFactor -= 0.1;
}
},
enabled: hasWorkspaces,
},
{ type: 'separator' },
{
label: 'Reload This Page',
accelerator: 'CmdOrCtrl+R',
click: (menuItem, browserWindow) => {
// if item is called in popup window
// open menu bar in the popup window instead
if (browserWindow && browserWindow.isPopup) {
browserWindow.webContents.reload();
return;
}
const win = mainWindow.get();
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
if (win !== null && win.getBrowserView() !== null) {
// @ts-expect-error ts-migrate(2532) FIXME: Object is possibly 'undefined'.
win.getBrowserView().webContents.reload();
}
},
enabled: hasWorkspaces,
},
{ type: 'separator' },
{
label: 'Developer Tools',
submenu: [
{
label: 'Open Developer Tools of Active Workspace',
accelerator: 'CmdOrCtrl+Option+I',
click: () => getActiveBrowserView().webContents.openDevTools(),
enabled: hasWorkspaces,
},
],
},
],
id: 'View',
},
// language menu
{
label: 'Language',
submenu: getLanguageMenu(),
id: 'Language',
},
{
label: 'History',
id: 'History',
},
{
label: 'Workspaces',
id: 'Workspaces',
submenu: [],
},
{
role: 'window',
id: 'window',
submenu: [{ role: 'minimize' }, { role: 'close' }, { type: 'separator' }, { role: 'front' }, { type: 'separator' }],
},
{
role: 'help',
id: 'help',
submenu: [
{
label: 'TiddlyGit Support',
@ -269,16 +115,16 @@ export class MenuService {
/**
* Insert provided sub menu items into menubar, so user and services can register custom menu items
* @param menuName Top level menu name to insert menu items
* @param menuID Top level menu name to insert menu items
* @param menuItems An array of menu item to insert
* @param afterSubMenu The name or role of a submenu you want your submenu insert after. `null` means inserted as first submenu item; `undefined` means inserted as last submenu item;
* @param afterSubMenu The `id` or `role` of a submenu you want your submenu insert after. `null` means inserted as first submenu item; `undefined` means inserted as last submenu item;
* @param withSeparator Need to insert a separator first, before insert menu items
*/
insertMenu(menuName: string, menuItems: MenuItemConstructorOptions[], afterSubMenu?: string | null, withSeparator = false): void {
insertMenu(menuID: string, menuItems: DeferredMenuItemConstructorOptions[], afterSubMenu?: string | null, withSeparator = false): void {
let foundMenuName = false;
// try insert menu into an existed menu's submenu
for (const menu of this.menuTemplate) {
if (menu.label === menuName || menu.role === menuName) {
if (menu.id === menuID) {
foundMenuName = true;
if (Array.isArray(menu.submenu)) {
if (afterSubMenu === undefined) {
@ -295,10 +141,12 @@ export class MenuService {
menu.submenu = [...menuItems, ...menu.submenu];
} else if (typeof afterSubMenu === 'string') {
// insert after afterSubMenu
const afterSubMenuIndex = menu.submenu.findIndex((item) => item.label === afterSubMenu || item.role === afterSubMenu);
const afterSubMenuIndex = menu.submenu.findIndex((item) => item.id === afterSubMenu || item.role === afterSubMenu);
if (afterSubMenuIndex === -1) {
throw new Error(
`You try to insert menu with afterSubMenu ${afterSubMenu}, but we can not found it in menu ${menu.label ?? menu.role ?? JSON.stringify(menu)}`,
`You try to insert menu with afterSubMenu ${afterSubMenu}, but we can not found it in menu ${
menu.id ?? menu.role ?? JSON.stringify(menu)
}, please specific a menuitem with correct id attribute`,
);
}
menu.submenu = [...take(menu.submenu, afterSubMenuIndex + 1), ...menuItems, ...drop(menu.submenu, afterSubMenuIndex - 1)];
@ -312,7 +160,7 @@ export class MenuService {
// if user wants to create a new menu in menubar
if (!foundMenuName) {
this.menuTemplate.push({
label: menuName,
label: menuID,
submenu: menuItems,
});
}

View file

@ -4,15 +4,19 @@ import { injectable, inject } from 'inversify';
import serviceIdentifiers from '@services/serviceIdentifier';
import { Preference } from '@services/preferences';
import { Workspace } from '@services/workspaces';
import { WorkspaceView } from '@services/workspacesView';
import { Wiki } from '@services/wiki';
import { Authentication } from '@services/auth';
import { Window } from '@services/windows';
import { MenuService } from '@services/menu';
import { WindowNames, IBrowserViewMetaData } from '@services/windows/WindowProperties';
import i18n from '../libs/i18n';
import getViewBounds from '@services/libs/get-view-bounds';
import { extractDomain } from '@services/libs/url';
import { IWorkspace } from '@services/types';
import setupViewEventHandlers from './setupViewEventHandlers';
import getFromRenderer from '@services/libs/getFromRenderer';
import { MetaDataChannel } from '@/constants/channels';
@injectable()
export class View {
@ -22,11 +26,14 @@ export class View {
@inject(serviceIdentifiers.Window) private readonly windowService: Window,
@inject(serviceIdentifiers.Workspace) private readonly workspaceService: Workspace,
@inject(serviceIdentifiers.Authentication) private readonly authService: Authentication,
@inject(serviceIdentifiers.MenuService) private readonly menuService: MenuService,
@inject(serviceIdentifiers.WorkspaceView) private readonly workspaceViewService: WorkspaceView,
) {
this.init();
this.initIPCHandlers();
this.registerMenu();
}
private init(): void {
private initIPCHandlers(): void {
// https://www.electronjs.org/docs/tutorial/online-offline-events
ipcMain.handle('online-status-changed', (_event, online: boolean) => {
if (online) {
@ -38,6 +45,162 @@ export class View {
});
}
private registerMenu(): void {
const hasWorkspaces = this.workspaceService.countWorkspaces() > 0;
this.menuService.insertMenu('View', [
{
label: () => (this.preferenceService.get('sidebar') ? 'Hide Sidebar' : 'Show Sidebar'),
accelerator: 'CmdOrCtrl+Alt+S',
click: () => {
void this.preferenceService.set('sidebar', !this.preferenceService.get('sidebar'));
void this.workspaceViewService.realignActiveWorkspace();
},
},
{
label: () => (this.preferenceService.get('navigationBar') ? 'Hide Navigation Bar' : 'Show Navigation Bar'),
accelerator: 'CmdOrCtrl+Alt+N',
click: () => {
void this.preferenceService.set('navigationBar', !this.preferenceService.get('navigationBar'));
void this.workspaceViewService.realignActiveWorkspace();
},
},
{
label: () => (this.preferenceService.get('titleBar') ? 'Hide Title Bar' : 'Show Title Bar'),
accelerator: 'CmdOrCtrl+Alt+T',
enabled: process.platform === 'darwin',
visible: process.platform === 'darwin',
click: () => {
void this.preferenceService.set('titleBar', !this.preferenceService.get('titleBar'));
void this.workspaceViewService.realignActiveWorkspace();
},
},
// same behavior as BrowserWindow with autoHideMenuBar: true
// but with addition to readjust BrowserView so it won't cover the menu bar
{
label: 'Toggle Menu Bar',
visible: false,
accelerator: 'Alt+M',
enabled: process.platform === 'win32',
click: async (_menuItem, browserWindow) => {
// if back is called in popup window
// open menu bar in the popup window instead
if (browserWindow === undefined) return;
const { isPopup } = await getFromRenderer<IBrowserViewMetaData>(MetaDataChannel.getViewMetaData, browserWindow);
if (isPopup === true) {
browserWindow.setMenuBarVisibility(!browserWindow.isMenuBarVisible());
return;
}
const mainWindow = this.windowService.get(WindowNames.main);
mainWindow?.setMenuBarVisibility(!mainWindow?.isMenuBarVisible());
void this.workspaceViewService.realignActiveWorkspace();
},
},
{ type: 'separator' },
{ role: 'togglefullscreen' },
{
label: 'Actual Size',
accelerator: 'CmdOrCtrl+0',
click: async (_menuItem, browserWindow) => {
// if item is called in popup window
// open menu bar in the popup window instead
if (browserWindow === undefined) return;
const { isPopup } = await getFromRenderer<IBrowserViewMetaData>(MetaDataChannel.getViewMetaData, browserWindow);
if (isPopup === true) {
const contents = browserWindow.webContents;
contents.zoomFactor = 1;
return;
}
const mainWindow = this.windowService.get(WindowNames.main);
const webContent = mainWindow?.getBrowserView()?.webContents;
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (webContent) {
webContent.setZoomFactor(1);
}
},
enabled: hasWorkspaces,
},
{
label: 'Zoom In',
accelerator: 'CmdOrCtrl+=',
click: async (_menuItem, browserWindow) => {
// if item is called in popup window
// open menu bar in the popup window instead
if (browserWindow === undefined) return;
const { isPopup } = await getFromRenderer<IBrowserViewMetaData>(MetaDataChannel.getViewMetaData, browserWindow);
if (isPopup === true) {
const contents = browserWindow.webContents;
contents.zoomFactor += 0.1;
return;
}
const mainWindow = this.windowService.get(WindowNames.main);
const webContent = mainWindow?.getBrowserView()?.webContents;
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (webContent) {
webContent.setZoomFactor(webContent.getZoomFactor() + 0.1);
}
},
enabled: hasWorkspaces,
},
{
label: 'Zoom Out',
accelerator: 'CmdOrCtrl+-',
click: async (_menuItem, browserWindow) => {
// if item is called in popup window
// open menu bar in the popup window instead
if (browserWindow === undefined) return;
const { isPopup } = await getFromRenderer<IBrowserViewMetaData>(MetaDataChannel.getViewMetaData, browserWindow);
if (isPopup === true) {
const contents = browserWindow.webContents;
contents.zoomFactor -= 0.1;
return;
}
const mainWindow = this.windowService.get(WindowNames.main);
const webContent = mainWindow?.getBrowserView()?.webContents;
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (webContent) {
webContent.setZoomFactor(webContent.getZoomFactor() - 0.1);
}
},
enabled: hasWorkspaces,
},
{ type: 'separator' },
{
label: 'Reload This Page',
accelerator: 'CmdOrCtrl+R',
click: async (_menuItem, browserWindow) => {
// if item is called in popup window
// open menu bar in the popup window instead
if (browserWindow === undefined) return;
const { isPopup } = await getFromRenderer<IBrowserViewMetaData>(MetaDataChannel.getViewMetaData, browserWindow);
if (isPopup === true) {
browserWindow.webContents.reload();
return;
}
const mainWindow = this.windowService.get(WindowNames.main);
const webContent = mainWindow?.getBrowserView()?.webContents;
// eslint-disable-next-line @typescript-eslint/strict-boolean-expressions
if (webContent) {
webContent.reload();
}
},
enabled: hasWorkspaces,
},
{ type: 'separator' },
{
label: 'Developer Tools',
submenu: [
{
label: 'Open Developer Tools of Active Workspace',
accelerator: 'CmdOrCtrl+Option+I',
click: () => this.getActiveBrowserView()?.webContents?.openDevTools(),
enabled: hasWorkspaces,
},
],
},
]);
}
private views: Record<string, BrowserView> = {};
private shouldMuteAudio = false;
private shouldPauseNotifications = false;
@ -83,8 +246,7 @@ export class View {
});
} else if (proxyType === 'pacScript') {
await sessionOfView.setProxy({
// @ts-expect-error ts-migrate(2345) FIXME: Argument of type '{ proxyPacScript: any; proxyBypa... Remove this comment to see the full error message
proxyPacScript,
pacScript: proxyPacScript,
proxyBypassRules,
});
}
@ -227,6 +389,7 @@ export class View {
const view = this.getView(id);
void session.fromPartition(`persist:${id}`).clearStorageData();
// FIXME: Property 'destroy' does not exist on type 'BrowserView'.ts(2339) , might related to https://github.com/electron/electron/pull/25411 which previously cause crush when I quit the app
// maybe use https://github.com/electron/electron/issues/10096
// if (view !== undefined) {
// view.destroy();
// }
@ -265,7 +428,6 @@ export class View {
public hibernateView = (id: string): void => {
if (this.getView(id) !== undefined) {
// FIXME: remove view
// @ts-expect-error Property 'destroy' does not exist on type 'BrowserView'.ts(2339)
// eslint-disable-next-line @typescript-eslint/no-unsafe-call
this.getView(id).destroy();
this.removeView(id);

View file

@ -9,6 +9,7 @@ import { buildResourcePath } from '@services/constants/paths';
import { Preference } from '@services/preferences';
import { Workspace } from '@services/workspaces';
import { WorkspaceView } from '@services/workspacesView';
import { Window } from '@services/windows';
import { WindowNames, IBrowserViewMetaData } from '@services/windows/WindowProperties';
import { container } from '@services/container';
@ -32,6 +33,7 @@ export default function setupViewEventHandlers(
{ adjustUserAgentByUrl }: IViewModifier,
): void {
const workspaceService = container.resolve(Workspace);
const workspaceViewService = container.resolve(WorkspaceView);
const windowService = container.resolve(Window);
const preferenceService = container.resolve(Preference);
@ -104,7 +106,7 @@ export default function setupViewEventHandlers(
lastUrl: currentUrl,
});
// fix https://github.com/atomery/webcatalog/issues/870
ipcMain.emit('request-realign-active-workspace');
workspaceViewService.realignActiveWorkspace();
});
// focus on initial load
// https://github.com/atomery/webcatalog/issues/398

View file

@ -260,13 +260,52 @@ export class Window {
'close',
);
const hasWorkspaces = this.workspaceService.countWorkspaces() > 0;
this.menuService.insertMenu(
'Edit',
[
{
label: 'Find',
accelerator: 'CmdOrCtrl+F',
click: () => {
const mainWindow = this.get(WindowNames.main);
if (mainWindow !== undefined) {
mainWindow.webContents.focus();
mainWindow.webContents.send('open-find-in-page');
const contentSize = mainWindow.getContentSize();
const view = mainWindow.getBrowserView();
view?.setBounds(getViewBounds(contentSize as [number, number], true));
}
},
enabled: () => this.workspaceService.countWorkspaces() > 0,
},
{
label: 'Find Next',
accelerator: 'CmdOrCtrl+G',
click: () => {
const mainWindow = this.get(WindowNames.main);
mainWindow?.webContents?.send('request-back-find-in-page', true);
},
enabled: () => this.workspaceService.countWorkspaces() > 0,
},
{
label: 'Find Previous',
accelerator: 'Shift+CmdOrCtrl+G',
click: () => {
const mainWindow = this.get(WindowNames.main);
mainWindow?.webContents?.send('request-back-find-in-page', false);
},
enabled: () => this.workspaceService.countWorkspaces() > 0,
},
],
'close',
);
this.menuService.insertMenu('History', [
{
label: 'Home',
accelerator: 'Shift+CmdOrCtrl+H',
click: () => ipcMain.emit('request-go-home'),
enabled: hasWorkspaces,
enabled: () => this.workspaceService.countWorkspaces() > 0,
},
{
label: 'Back',
@ -284,7 +323,7 @@ export class Window {
}
ipcMain.emit('request-go-back');
},
enabled: hasWorkspaces,
enabled: () => this.workspaceService.countWorkspaces() > 0,
},
{
label: 'Forward',
@ -301,7 +340,7 @@ export class Window {
}
ipcMain.emit('request-go-forward');
},
enabled: hasWorkspaces,
enabled: () => this.workspaceService.countWorkspaces() > 0,
},
{ type: 'separator' },
{
@ -324,7 +363,7 @@ export class Window {
clipboard.writeText(url);
}
},
enabled: hasWorkspaces,
enabled: () => this.workspaceService.countWorkspaces() > 0,
},
]);
}

View file

@ -229,16 +229,16 @@ export const createAsync = async (): Promise<void> =>
// after the UI is fully loaded
// if not, BrowserView mouseover event won't work correctly
// https://github.com/atomery/webcatalog/issues/812
ipcMain.emit('request-realign-active-workspace');
this.workspaceViewService.realignActiveWorkspace();
});
win.on('enter-full-screen', () => {
win?.webContents.send('is-fullscreen-updated', true);
ipcMain.emit('request-realign-active-workspace');
this.workspaceViewService.realignActiveWorkspace();
});
win.on('leave-full-screen', () => {
win?.webContents.send('is-fullscreen-updated', false);
ipcMain.emit('request-realign-active-workspace');
this.workspaceViewService.realignActiveWorkspace();
});
// ensure redux is loaded first

View file

@ -5,11 +5,10 @@ import serviceIdentifiers from '@services/serviceIdentifier';
import { View } from '@services/view';
import { Workspace } from '@services/workspaces';
import { Window } from '@services/windows';
import sendToAllWindows from '@services/libs/send-to-all-windows';
import { MenuService } from '@services/menu';
import { IWorkspace } from '@services/types';
import { WindowNames } from '@services/windows/WindowProperties';
import { Preference } from '@services/preferences';
import createMenu from '@services/libs/create-menu';
/**
* Deal with operations that needs to create a workspace and a browserView at once
@ -21,41 +20,31 @@ export class WorkspaceView {
@inject(serviceIdentifiers.Workspace) private readonly workspaceService: Workspace,
@inject(serviceIdentifiers.Window) private readonly windowService: Window,
@inject(serviceIdentifiers.Preference) private readonly preferenceService: Preference,
@inject(serviceIdentifiers.MenuService) private readonly menuService: MenuService,
) {
this.init();
this.initIPCHandlers();
this.registerMenu();
}
private init(): void {
private initIPCHandlers(): void {
ipcMain.handle('request-create-workspace', async (_event, workspaceOptions: IWorkspace) => {
await this.createWorkspaceView(workspaceOptions);
createMenu();
this.menuService.buildMenu();
});
ipcMain.handle('request-set-active-workspace', async (_event, id) => {
if (this.workspaceService.get(id) !== undefined) {
await this.setActiveWorkspaceView(id);
createMenu();
this.menuService.buildMenu();
}
});
ipcMain.handle('request-get-active-workspace', (_event) => {
return this.workspaceService.getActiveWorkspace();
});
ipcMain.handle('request-realign-active-workspace', () => {
const { sidebar, titleBar, navigationBar } = this.preferenceService.getPreferences();
// FIXME: global usage
global.sidebar = sidebar;
global.titleBar = titleBar;
global.navigationBar = navigationBar;
// this function only call browserView.setBounds
// do not attempt to recall browserView.webContents.focus()
// as it breaks page focus (cursor, scroll bar not visible)
this.realignActiveWorkspaceView();
createMenu();
});
ipcMain.handle('request-open-url-in-workspace', async (_event, url: string, id: string) => {
if (typeof id === 'string' && id.length > 0) {
// if id is defined, switch to that workspace
await this.setActiveWorkspaceView(id);
createMenu();
this.menuService.buildMenu();
// load url in the current workspace
const activeWorkspace = this.workspaceService.getActiveWorkspace();
if (activeWorkspace !== undefined) {
@ -72,17 +61,38 @@ export class WorkspaceView {
ipcMain.handle('request-set-workspace', async (_event, id, options) => {
await this.setWorkspaceView(id, options);
createMenu();
this.menuService.buildMenu();
});
ipcMain.handle('request-set-workspaces', async (_event, workspaces) => {
await this.setWorkspaceViews(workspaces);
createMenu();
this.menuService.buildMenu();
});
ipcMain.handle('request-load-url', async (_event, url, id) => {
await this.loadURL(url, id);
});
}
private registerMenu(): void {
const hasWorkspaces = this.workspaceService.countWorkspaces() > 0;
this.menuService.insertMenu(
'window',
[
{
label: 'Developer Tools',
submenu: [
{
label: 'Open Developer Tools of Active Workspace',
accelerator: 'CmdOrCtrl+Option+I',
click: () => this.viewService.getActiveBrowserView()?.webContents?.openDevTools(),
enabled: hasWorkspaces,
},
],
},
],
'close',
);
}
public async createWorkspaceView(workspaceOptions: IWorkspace): Promise<void> {
const newWorkspace = await this.workspaceService.create(workspaceOptions);
const mainWindow = this.windowService.get(WindowNames.main);
@ -154,7 +164,7 @@ export class WorkspaceView {
// eslint-disable-next-line unicorn/no-null
mainWindow.setBrowserView(null);
mainWindow.setTitle(app.name);
sendToAllWindows('update-title', '');
this.windowService.sendToAllWindows('update-title', '');
}
} else if (this.workspaceService.countWorkspaces() > 1 && this.workspaceService.get(id)?.active === true) {
const previousWorkspace = this.workspaceService.getPreviousWorkspace(id);
@ -190,7 +200,20 @@ export class WorkspaceView {
}
}
public realignActiveWorkspaceView(): void {
/**
* Seems this is for relocating BrowserView in the electron window
* // TODO: why we need this?
*/
public realignActiveWorkspace(): void {
// this function only call browserView.setBounds
// do not attempt to recall browserView.webContents.focus()
// as it breaks page focus (cursor, scroll bar not visible)
this.realignActiveWorkspaceView();
// TODO: why we need to rebuild menu?
this.menuService.buildMenu();
}
private realignActiveWorkspaceView(): void {
const activeWorkspace = this.workspaceService.getActiveWorkspace();
const mainWindow = this.windowService.get(WindowNames.main);
if (activeWorkspace !== undefined && mainWindow !== undefined) {