summaryrefslogtreecommitdiffhomepage
path: root/gui/src
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2020-11-09 18:11:46 +0100
committerOskar Nyberg <oskar@mullvad.net>2020-11-19 11:22:02 +0100
commit585f019300df75cdac366a7ecbdf7f9b8184cce1 (patch)
tree9875e6ad66d1394b23235297f5727eb097108f24 /gui/src
parent9790676f189a5c68c9478e8561e4911a6926b932 (diff)
downloadmullvadvpn-585f019300df75cdac366a7ecbdf7f9b8184cce1.tar.xz
mullvadvpn-585f019300df75cdac366a7ecbdf7f9b8184cce1.zip
Adjust app to handle window mode
Diffstat (limited to 'gui/src')
-rw-r--r--gui/src/main/index.ts250
-rw-r--r--gui/src/main/window-controller.ts26
-rw-r--r--gui/src/renderer/components/HeaderBar.tsx20
-rw-r--r--gui/src/renderer/components/NavigationBar.tsx10
-rw-r--r--gui/src/renderer/components/NavigationBarStyles.tsx6
-rw-r--r--gui/src/renderer/components/PlatformWindow.tsx11
-rw-r--r--gui/src/renderer/containers/PlatformWindowContainer.tsx1
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);