diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2020-11-09 18:11:46 +0100 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2020-11-19 11:22:02 +0100 |
| commit | 585f019300df75cdac366a7ecbdf7f9b8184cce1 (patch) | |
| tree | 9875e6ad66d1394b23235297f5727eb097108f24 /gui | |
| parent | 9790676f189a5c68c9478e8561e4911a6926b932 (diff) | |
| download | mullvadvpn-585f019300df75cdac366a7ecbdf7f9b8184cce1.tar.xz mullvadvpn-585f019300df75cdac366a7ecbdf7f9b8184cce1.zip | |
Adjust app to handle window mode
Diffstat (limited to 'gui')
| -rw-r--r-- | gui/src/main/index.ts | 250 | ||||
| -rw-r--r-- | gui/src/main/window-controller.ts | 26 | ||||
| -rw-r--r-- | gui/src/renderer/components/HeaderBar.tsx | 20 | ||||
| -rw-r--r-- | gui/src/renderer/components/NavigationBar.tsx | 10 | ||||
| -rw-r--r-- | gui/src/renderer/components/NavigationBarStyles.tsx | 6 | ||||
| -rw-r--r-- | gui/src/renderer/components/PlatformWindow.tsx | 11 | ||||
| -rw-r--r-- | gui/src/renderer/containers/PlatformWindowContainer.tsx | 1 |
7 files changed, 197 insertions, 127 deletions
diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts index d780e8e128..10f66b3f08 100644 --- a/gui/src/main/index.ts +++ b/gui/src/main/index.ts @@ -353,7 +353,7 @@ class ApplicationMain { const window = this.createWindow(); const tray = this.createTray(); - const windowController = new WindowController(window, tray); + const windowController = new WindowController(window, tray, this.guiSettings.unpinnedWindow); this.tunnelStateExpectation = new Expectation(() => { this.trayIconController = new TrayIconController( tray, @@ -362,9 +362,7 @@ class ApplicationMain { ); }); - this.registerWindowListener(windowController); this.registerIpcListeners(); - this.addContextMenu(window); this.windowController = windowController; this.tray = tray; @@ -385,43 +383,50 @@ class ApplicationMain { } }; - if (process.env.NODE_ENV === 'development') { - await this.installDevTools(); - window.webContents.openDevTools({ mode: 'detach' }); + if (this.shouldShowWindowOnStart() || process.env.NODE_ENV === 'development') { + windowController.show(); } - switch (process.platform) { - case 'win32': - this.installWindowsMenubarAppWindowHandlers(tray, windowController); - break; - case 'darwin': - this.installMacOsMenubarAppWindowHandlers(tray, windowController); - this.setMacOsAppMenu(); - break; - case 'linux': - this.installLinuxMenubarAppWindowHandlers(tray, windowController); - this.setLinuxTrayContextMenu(); - this.installLinuxWindowCloseHandler(windowController); - this.setLinuxAppMenu(); - window.setMenuBarVisibility(false); - break; - default: - this.installGenericMenubarAppWindowHandlers(tray, windowController); - break; - } + await this.initializeWindow(); + }; - this.installGenericFocusHandlers(windowController); + private async initializeWindow() { + if (this.windowController && this.tray) { + this.registerWindowListener(this.windowController); + this.addContextMenu(this.windowController.window); - if (this.shouldShowWindowOnStart() || process.env.NODE_ENV === 'development') { - windowController.show(); - } + if (process.env.NODE_ENV === 'development') { + await this.installDevTools(); + this.windowController.window.webContents.openDevTools({ mode: 'detach' }); + } - try { - await window.loadFile(path.resolve(path.join(__dirname, '../renderer/index.html'))); - } catch (error) { - log.error(`Failed to load index file: ${error.message}`); + switch (process.platform) { + case 'win32': + this.installWindowsMenubarAppWindowHandlers(this.tray, this.windowController); + break; + case 'darwin': + this.installMacOsMenubarAppWindowHandlers(this.windowController); + this.setMacOsAppMenu(); + break; + case 'linux': + this.tray.setContextMenu(this.createTrayContextMenu()); + this.setLinuxAppMenu(); + this.windowController.window.setMenuBarVisibility(false); + break; + } + + this.installWindowCloseHandler(this.windowController); + this.installTrayClickHandlers(); + this.installGenericFocusHandlers(this.windowController); + + const filePath = path.resolve(path.join(__dirname, '../renderer/index.html')); + try { + await this.windowController?.window.loadFile(filePath); + } catch (error) { + log.error(`Failed to load index file: ${error.message}`); + } } - }; + } private onDaemonConnected = async () => { this.connectedToDaemon = true; @@ -639,7 +644,7 @@ class ApplicationMain { consumePromise(this.updateLocation()); if (process.platform === 'linux') { - this.setLinuxTrayContextMenu(); + this.tray?.setContextMenu(this.createTrayContextMenu()); } this.notificationController.notifyTunnelState( @@ -1016,6 +1021,10 @@ class ApplicationMain { this.guiSettings.monochromaticIcon = monochromaticIcon; }); + IpcMainEventChannel.guiSettings.handleSetUnpinnedWindow((unpinnedWindow: boolean) => { + consumePromise(this.setUnpinnedWindow(unpinnedWindow)); + }); + IpcMainEventChannel.guiSettings.handleSetPreferredLocale((locale: string) => { this.guiSettings.preferredLocale = locale; this.didChangeLocale(); @@ -1318,6 +1327,20 @@ class ApplicationMain { return Promise.resolve(); } + private async setUnpinnedWindow(unpinnedWindow: boolean): Promise<void> { + this.guiSettings.unpinnedWindow = unpinnedWindow; + + if (this.tray && this.windowController) { + this.tray.removeAllListeners(); + + const window = this.createWindow(); + this.windowController.replaceWindow(window, unpinnedWindow); + + await this.initializeWindow(); + this.windowController.show(); + } + } + private updateCurrentLocale() { this.locale = this.detectLocale(); @@ -1350,20 +1373,27 @@ class ApplicationMain { private createWindow(): BrowserWindow { const contentHeight = 568; - // the size of transparent area around arrow on macOS const headerBarArrowHeight = 12; + const height = + process.platform === 'darwin' && !this.guiSettings.unpinnedWindow + ? contentHeight + headerBarArrowHeight + : contentHeight; const options: Electron.BrowserWindowConstructorOptions = { width: 320, minWidth: 320, - height: contentHeight, - minHeight: contentHeight, + height, + minHeight: height, resizable: false, maximizable: false, fullscreenable: false, show: false, - frame: false, + frame: this.guiSettings.unpinnedWindow, + transparent: !this.guiSettings.unpinnedWindow, + minimizable: this.guiSettings.unpinnedWindow, + closable: this.guiSettings.unpinnedWindow, + useContentSize: true, webPreferences: { nodeIntegration: true, devTools: process.env.NODE_ENV === 'development', @@ -1377,14 +1407,13 @@ class ApplicationMain { ...options, height: contentHeight + headerBarArrowHeight, minHeight: contentHeight + headerBarArrowHeight, - transparent: true, - titleBarStyle: 'customButtonsOnHover', - minimizable: false, - closable: false, + titleBarStyle: this.guiSettings.unpinnedWindow ? 'default' : 'customButtonsOnHover', }); // make the window visible on all workspaces - appWindow.setVisibleOnAllWorkspaces(true); + if (!this.guiSettings.unpinnedWindow) { + appWindow.setVisibleOnAllWorkspaces(true); + } return appWindow; } @@ -1396,18 +1425,13 @@ class ApplicationMain { // Due to a bug in Electron the app is sometimes placed behind other apps when opened. // Setting alwaysOnTop to true ensures that the app is placed on top. Electron issue: // https://github.com/electron/electron/issues/25915 - alwaysOnTop: true, - transparent: true, - skipTaskbar: true, + alwaysOnTop: !this.guiSettings.unpinnedWindow, + skipTaskbar: !this.guiSettings.unpinnedWindow, + autoHideMenuBar: true, }); default: { - const appWindow = new BrowserWindow({ - ...options, - frame: true, - }); - - return appWindow; + return new BrowserWindow(options); } } } @@ -1449,7 +1473,7 @@ class ApplicationMain { Menu.setApplicationMenu(Menu.buildFromTemplate(template)); } - private setLinuxTrayContextMenu() { + private createTrayContextMenu() { const template: Electron.MenuItemConstructorOptions[] = [ { label: sprintf(messages.pgettext('tray-icon-context-menu', 'Open %(mullvadVpn)s'), { @@ -1459,7 +1483,7 @@ class ApplicationMain { }, { type: 'separator' }, { - label: this.getLinuxContextMenuActionButtonLabel(), + label: this.getContextMenuActionButtonLabel(), click: () => { if (this.tunnelState.state === 'disconnected') { // Workaround: gRPC calls are sometimes delayed by a few seconds and setImmediate @@ -1477,10 +1501,10 @@ class ApplicationMain { }, ]; - this.tray?.setContextMenu(Menu.buildFromTemplate(template)); + return Menu.buildFromTemplate(template); } - private getLinuxContextMenuActionButtonLabel() { + private getContextMenuActionButtonLabel() { switch (this.tunnelState.state) { case 'disconnected': return messages.gettext('Connect'); @@ -1551,65 +1575,71 @@ class ApplicationMain { return tray; } - private installWindowsMenubarAppWindowHandlers(tray: Tray, windowController: WindowController) { - tray.on('click', () => windowController.toggle()); - tray.on('right-click', () => windowController.hide()); - - windowController.window.on('blur', () => { - // Detect if blur happened when user had a cursor above the tray icon. - const trayBounds = tray.getBounds(); - const cursorPos = screen.getCursorScreenPoint(); - const isCursorInside = - cursorPos.x >= trayBounds.x && - cursorPos.y >= trayBounds.y && - cursorPos.x <= trayBounds.x + trayBounds.width && - cursorPos.y <= trayBounds.y + trayBounds.height; - if (!isCursorInside) { - windowController.hide(); + private installTrayClickHandlers() { + if (this.guiSettings.unpinnedWindow) { + if (process.platform === 'win32' || process.platform === 'darwin') { + this.tray?.on('right-click', () => + // This needs to be executed on click since if it is added to the tray icon it will be + // displayed on left click as well. + this.tray?.popUpContextMenu(this.createTrayContextMenu()), + ); } - }); + this.tray?.on('click', () => this.windowController?.show()); + } else { + this.tray?.on('click', () => this.windowController?.toggle()); + this.tray?.on('right-click', () => this.windowController?.hide()); + } } - // setup NSEvent monitor to fix inconsistent window.blur on macOS - // see https://github.com/electron/electron/issues/8689 - private installMacOsMenubarAppWindowHandlers(tray: Tray, windowController: WindowController) { - // eslint-disable-next-line @typescript-eslint/no-var-requires - const { NSEventMonitor, NSEventMask } = require('nseventmonitor'); - const macEventMonitor = new NSEventMonitor(); - const eventMask = NSEventMask.leftMouseDown | NSEventMask.rightMouseDown; - const window = windowController.window; - - window.on('show', () => macEventMonitor.start(eventMask, () => windowController.hide())); - window.on('hide', () => macEventMonitor.stop()); - window.on('blur', () => { - // Make sure to hide the menubar window when other program captures the focus. - // But avoid doing that when dev tools capture the focus to make it possible to inspect the UI - if (window.isVisible() && !window.webContents.isDevToolsFocused()) { - windowController.hide(); - } - }); - tray.on('click', () => windowController.toggle()); + private installWindowsMenubarAppWindowHandlers(tray: Tray, windowController: WindowController) { + if (!this.guiSettings.unpinnedWindow) { + windowController.window.on('blur', () => { + // Detect if blur happened when user had a cursor above the tray icon. + const trayBounds = tray.getBounds(); + const cursorPos = screen.getCursorScreenPoint(); + const isCursorInside = + cursorPos.x >= trayBounds.x && + cursorPos.y >= trayBounds.y && + cursorPos.x <= trayBounds.x + trayBounds.width && + cursorPos.y <= trayBounds.y + trayBounds.height; + if (!isCursorInside) { + windowController.hide(); + } + }); + } } - private installGenericMenubarAppWindowHandlers(tray: Tray, windowController: WindowController) { - tray.on('click', () => { - windowController.toggle(); - }); - } + // setup NSEvent monitor to fix inconsistent window.blur on macOS + // see https://github.com/electron/electron/issues/8689 + private installMacOsMenubarAppWindowHandlers(windowController: WindowController) { + if (!this.guiSettings.unpinnedWindow) { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const { NSEventMonitor, NSEventMask } = require('nseventmonitor'); + const macEventMonitor = new NSEventMonitor(); + const eventMask = NSEventMask.leftMouseDown | NSEventMask.rightMouseDown; + const window = windowController.window; - private installLinuxMenubarAppWindowHandlers(tray: Tray, windowController: WindowController) { - tray.on('click', () => { - windowController.show(); - }); + window.on('show', () => macEventMonitor.start(eventMask, () => windowController.hide())); + window.on('hide', () => macEventMonitor.stop()); + window.on('blur', () => { + // Make sure to hide the menubar window when other program captures the focus. + // But avoid doing that when dev tools capture the focus to make it possible to inspect the UI + if (window.isVisible() && !window.webContents.isDevToolsFocused()) { + windowController.hide(); + } + }); + } } - private installLinuxWindowCloseHandler(windowController: WindowController) { - windowController.window.on('close', (closeEvent: Event) => { - if (this.quitStage !== AppQuitStage.ready) { - closeEvent.preventDefault(); - windowController.hide(); - } - }); + private installWindowCloseHandler(windowController: WindowController) { + if (this.guiSettings.unpinnedWindow) { + windowController.window.on('close', (closeEvent: Event) => { + if (this.quitStage !== AppQuitStage.ready) { + closeEvent.preventDefault(); + windowController.hide(); + } + }); + } } private installGenericFocusHandlers(windowController: WindowController) { @@ -1624,11 +1654,9 @@ class ApplicationMain { private shouldShowWindowOnStart(): boolean { switch (process.platform) { case 'win32': - return false; case 'darwin': - return false; case 'linux': - return !this.guiSettings.startMinimized; + return this.guiSettings.unpinnedWindow && !this.guiSettings.startMinimized; default: return true; } diff --git a/gui/src/main/window-controller.ts b/gui/src/main/window-controller.ts index 69457a63fa..4c4b744b79 100644 --- a/gui/src/main/window-controller.ts +++ b/gui/src/main/window-controller.ts @@ -133,6 +133,7 @@ class AttachedToTrayWindowPositioning implements IWindowPositioning { export default class WindowController { private width: number; private height: number; + private windowValue: BrowserWindow; private webContentsValue: WebContents; private windowPositioning: IWindowPositioning; private isWindowReady = false; @@ -145,20 +146,35 @@ export default class WindowController { return this.webContentsValue; } - constructor(private windowValue: BrowserWindow, tray: Tray) { + constructor(windowValue: BrowserWindow, private tray: Tray, unpinnedWindow: boolean) { const [width, height] = windowValue.getSize(); this.width = width; this.height = height; + this.windowValue = windowValue; this.webContentsValue = windowValue.webContents; - this.windowPositioning = - process.platform === 'linux' - ? new StandaloneWindowPositioning() - : new AttachedToTrayWindowPositioning(tray); + this.windowPositioning = unpinnedWindow + ? new StandaloneWindowPositioning() + : new AttachedToTrayWindowPositioning(tray); this.installDisplayMetricsHandler(); this.installWindowReadyHandlers(); } + public replaceWindow(window: BrowserWindow, unpinnedWindow: boolean) { + this.window.removeAllListeners(); + this.window.destroy(); + + this.windowValue = window; + this.webContentsValue = window.webContents; + + this.windowPositioning = unpinnedWindow + ? new StandaloneWindowPositioning() + : new AttachedToTrayWindowPositioning(this.tray); + + this.updatePosition(); + this.notifyUpdateWindowShape(); + } + public show(whenReady = true) { if (whenReady) { this.executeWhenWindowIsReady(() => this.showImmediately()); diff --git a/gui/src/renderer/components/HeaderBar.tsx b/gui/src/renderer/components/HeaderBar.tsx index 88b1200277..67b9622b21 100644 --- a/gui/src/renderer/components/HeaderBar.tsx +++ b/gui/src/renderer/components/HeaderBar.tsx @@ -1,8 +1,10 @@ import React, { useCallback } from 'react'; +import { useSelector } from 'react-redux'; import { useHistory } from 'react-router'; import styled from 'styled-components'; import { colors } from '../../config.json'; import { messages } from '../../shared/gettext'; +import { IReduxState } from '../redux/store'; import ImageView from './ImageView'; export enum HeaderBarStyle { @@ -19,9 +21,14 @@ const headerBarStyleColorMap = { [HeaderBarStyle.success]: colors.green, }; -const HeaderBarContainer = styled.header({}, (props: { barStyle?: HeaderBarStyle }) => ({ +interface IHeaderBarContainerProps { + barStyle?: HeaderBarStyle; + unpinnedWindow: boolean; +} + +const HeaderBarContainer = styled.header({}, (props: IHeaderBarContainerProps) => ({ padding: '12px 16px', - paddingTop: process.platform === 'darwin' ? '24px' : '12px', + paddingTop: process.platform === 'darwin' && !props.unpinnedWindow ? '24px' : '12px', backgroundColor: headerBarStyleColorMap[props.barStyle ?? HeaderBarStyle.default], })); @@ -40,8 +47,15 @@ interface IHeaderBarProps { } export default function HeaderBar(props: IHeaderBarProps) { + const unpinnedWindow = useSelector( + (state: IReduxState) => state.settings.guiSettings.unpinnedWindow, + ); + return ( - <HeaderBarContainer barStyle={props.barStyle} className={props.className}> + <HeaderBarContainer + barStyle={props.barStyle} + className={props.className} + unpinnedWindow={unpinnedWindow}> <HeaderBarContent>{props.children}</HeaderBarContent> </HeaderBarContainer> ); diff --git a/gui/src/renderer/components/NavigationBar.tsx b/gui/src/renderer/components/NavigationBar.tsx index 0e242ef3c4..746fc6faa4 100644 --- a/gui/src/renderer/components/NavigationBar.tsx +++ b/gui/src/renderer/components/NavigationBar.tsx @@ -178,6 +178,9 @@ interface INavigationBarProps { export const NavigationBar = function NavigationBarT(props: INavigationBarProps) { const { showsBarSeparator, showsBarTitle } = useContext(NavigationScrollContext); + const unpinnedWindow = useSelector( + (state: IReduxState) => state.settings.guiSettings.unpinnedWindow, + ); const [titleAdjustment, setTitleAdjustment] = useState(0); const titleContainerRef = useRef() as React.RefObject<HTMLDivElement>; @@ -217,7 +220,7 @@ export const NavigationBar = function NavigationBarT(props: INavigationBarProps) }); return ( - <StyledNavigationBar> + <StyledNavigationBar unpinnedWindow={unpinnedWindow}> <StyledNavigationBarWrapper ref={navigationBarRef}> <TitleBarItemContext.Provider value={{ @@ -266,7 +269,10 @@ interface ICloseBarItemProps { export function CloseBarItem(props: ICloseBarItemProps) { // Use the arrow down icon on Linux, to avoid confusion with the close button in the window // title bar. - const iconName = process.platform === 'linux' ? 'icon-close-down' : 'icon-close'; + const unpinnedWindow = useSelector( + (state: IReduxState) => state.settings.guiSettings.unpinnedWindow, + ); + const iconName = unpinnedWindow ? 'icon-close-down' : 'icon-close'; return ( <StyledCloseBarItemButton aria-label={messages.gettext('Close')} onClick={props.action}> <StyledCloseBarItemIcon diff --git a/gui/src/renderer/components/NavigationBarStyles.tsx b/gui/src/renderer/components/NavigationBarStyles.tsx index 3f964c1f93..98c5184ea3 100644 --- a/gui/src/renderer/components/NavigationBarStyles.tsx +++ b/gui/src/renderer/components/NavigationBarStyles.tsx @@ -17,11 +17,11 @@ export const StyledNavigationItems = styled.div({ flexDirection: 'row', }); -export const StyledNavigationBar = styled.nav({ +export const StyledNavigationBar = styled.nav((props: { unpinnedWindow: boolean }) => ({ flex: 0, padding: '12px', - paddingTop: process.platform === 'darwin' ? '24px' : '12px', -}); + paddingTop: process.platform === 'darwin' && !props.unpinnedWindow ? '24px' : '12px', +})); export const StyledNavigationBarWrapper = styled.div({ display: 'flex', diff --git a/gui/src/renderer/components/PlatformWindow.tsx b/gui/src/renderer/components/PlatformWindow.tsx index 53dcb9e8c3..633b7aee95 100644 --- a/gui/src/renderer/components/PlatformWindow.tsx +++ b/gui/src/renderer/components/PlatformWindow.tsx @@ -2,12 +2,17 @@ import styled from 'styled-components'; const ARROW_WIDTH = 30; -export default styled.div({}, ({ arrowPosition }: { arrowPosition?: number }) => { +interface IPlatformWindowProps { + arrowPosition?: number; + unpinnedWindow: boolean; +} + +export default styled.div({}, (props: IPlatformWindowProps) => { let mask: string | undefined; - if (process.platform === 'darwin') { + if (process.platform === 'darwin' && !props.unpinnedWindow) { const arrowPositionCss = - arrowPosition !== undefined ? `${arrowPosition - ARROW_WIDTH * 0.5}px` : '50%'; + props.arrowPosition !== undefined ? `${props.arrowPosition - ARROW_WIDTH * 0.5}px` : '50%'; mask = [ `url(../../assets/images/app-triangle.svg) ${arrowPositionCss} 0% no-repeat`, diff --git a/gui/src/renderer/containers/PlatformWindowContainer.tsx b/gui/src/renderer/containers/PlatformWindowContainer.tsx index f39cd82600..e929836f7d 100644 --- a/gui/src/renderer/containers/PlatformWindowContainer.tsx +++ b/gui/src/renderer/containers/PlatformWindowContainer.tsx @@ -5,6 +5,7 @@ import { IReduxState } from '../redux/store'; const mapStateToProps = (state: IReduxState) => ({ arrowPosition: state.userInterface.arrowPosition, + unpinnedWindow: state.settings.guiSettings.unpinnedWindow, }); export default connect(mapStateToProps)(PlatformWindow); |
