diff --git a/.vscode/settings.json b/.vscode/settings.json index 4d699c47..96e076fd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,7 @@ "fullscreenable", "maximizable", "minimizable", + "submenu", "subwiki", "subwiki's", "tiddlywiki's" diff --git a/src/main.ts b/src/main.ts index be0ef637..1c89dc03 100755 --- a/src/main.ts +++ b/src/main.ts @@ -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', () => { diff --git a/src/services/libs/create-menu.ts b/src/services/libs/create-menu.ts index 4b09e470..32e06041 100755 --- a/src/services/libs/create-menu.ts +++ b/src/services/libs/create-menu.ts @@ -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 { diff --git a/src/services/libs/i18n/i18next-electron-fs-backend.ts b/src/services/libs/i18n/i18next-electron-fs-backend.ts index 4319d5cc..2151c5ad 100644 --- a/src/services/libs/i18n/i18next-electron-fs-backend.ts +++ b/src/services/libs/i18n/i18next-electron-fs-backend.ts @@ -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); } diff --git a/src/services/libs/log/renderer-transport.ts b/src/services/libs/log/renderer-transport.ts index 6496bb4b..975aaadd 100644 --- a/src/services/libs/log/renderer-transport.ts +++ b/src/services/libs/log/renderer-transport.ts @@ -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); } diff --git a/src/services/menu.ts b/src/services/menu.ts index ec218033..4ef8cedc 100644 --- a/src/services/menu.ts +++ b/src/services/menu.ts @@ -5,19 +5,43 @@ import serviceIdentifiers from '@services/serviceIdentifier'; import { Preference } from '@services/preferences'; import { View } from '@services/view'; +interface DeferredMenuItemConstructorOptions extends Omit { + label?: (() => string) | string; + enabled?: (() => boolean) | boolean; + submenu?: + | (() => Array) + | Array; +} + @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 = 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, }); } diff --git a/src/services/view/index.ts b/src/services/view/index.ts index 5426d22a..002a755a 100644 --- a/src/services/view/index.ts +++ b/src/services/view/index.ts @@ -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(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(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(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(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(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 = {}; 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); diff --git a/src/services/view/setupViewEventHandlers.ts b/src/services/view/setupViewEventHandlers.ts index cbd61b90..dc58a305 100644 --- a/src/services/view/setupViewEventHandlers.ts +++ b/src/services/view/setupViewEventHandlers.ts @@ -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 diff --git a/src/services/windows/index.ts b/src/services/windows/index.ts index 6b3fabff..6913108c 100644 --- a/src/services/windows/index.ts +++ b/src/services/windows/index.ts @@ -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, }, ]); } diff --git a/src/services/windows/main.ts b/src/services/windows/main.ts index dbcff2fe..bfbd7532 100644 --- a/src/services/windows/main.ts +++ b/src/services/windows/main.ts @@ -229,16 +229,16 @@ export const createAsync = async (): Promise => // 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 diff --git a/src/services/workspacesView.ts b/src/services/workspacesView.ts index 7387cf31..e2138e3e 100644 --- a/src/services/workspacesView.ts +++ b/src/services/workspacesView.ts @@ -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 { 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) {