summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2020-11-19 11:45:59 +0100
committerOskar Nyberg <oskar@mullvad.net>2020-11-19 11:45:59 +0100
commita51e006ddfcc9a49ad4a4bf448a58bf2d4f5bf6a (patch)
treed0f03acb035e87e1bc4f415878b9bb8d482c9157
parentb72f29cd885c2e9c02b27f72cd93683aba253e8d (diff)
parent9f9eb140f0f7eab42bae361be6202418c59475f5 (diff)
downloadmullvadvpn-a51e006ddfcc9a49ad4a4bf448a58bf2d4f5bf6a.tar.xz
mullvadvpn-a51e006ddfcc9a49ad4a4bf448a58bf2d4f5bf6a.zip
Merge branch 'add-app-window-setting'
-rw-r--r--CHANGELOG.md3
-rw-r--r--gui/src/main/gui-settings.ts10
-rw-r--r--gui/src/main/index.ts250
-rw-r--r--gui/src/main/window-controller.ts49
-rw-r--r--gui/src/renderer/app.tsx4
-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/components/Preferences.tsx36
-rw-r--r--gui/src/renderer/containers/PlatformWindowContainer.tsx1
-rw-r--r--gui/src/renderer/containers/PreferencesPage.tsx5
-rw-r--r--gui/src/renderer/redux/settings/reducers.ts1
-rw-r--r--gui/src/shared/gui-settings-state.ts3
-rw-r--r--gui/src/shared/ipc-event-channel.ts5
15 files changed, 279 insertions, 135 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c7bcd6ad37..800512c7a6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -30,6 +30,9 @@ Line wrap the file at 100 chars. Th
- Navigate back to the main view when escape is pressed.
- Add support for custom DNS resolvers.
+#### Windows
+- Add setting that unpins the window from the tray icon to let the user move it around freely.
+
#### Linux
- Optionally use NetworkManager to create WireGuard devices.
- Disable NetworkManager's connectivity check before applying firewall rules to avoid triggerring
diff --git a/gui/src/main/gui-settings.ts b/gui/src/main/gui-settings.ts
index d1d620ba54..e7d8a47cf1 100644
--- a/gui/src/main/gui-settings.ts
+++ b/gui/src/main/gui-settings.ts
@@ -10,6 +10,7 @@ const settingsSchema = {
enableSystemNotifications: 'boolean',
monochromaticIcon: 'boolean',
startMinimized: 'boolean',
+ unpinnedWindow: 'boolean',
};
const defaultSettings: IGuiSettingsState = {
@@ -18,6 +19,7 @@ const defaultSettings: IGuiSettingsState = {
enableSystemNotifications: true,
monochromaticIcon: false,
startMinimized: false,
+ unpinnedWindow: process.platform !== 'win32' && process.platform !== 'darwin',
};
export default class GuiSettings {
@@ -65,6 +67,14 @@ export default class GuiSettings {
return this.stateValue.startMinimized;
}
+ set unpinnedWindow(newValue: boolean) {
+ this.changeStateAndNotify({ ...this.stateValue, unpinnedWindow: newValue });
+ }
+
+ get unpinnedWindow(): boolean {
+ return this.stateValue.unpinnedWindow;
+ }
+
public onChange?: (newState: IGuiSettingsState, oldState: IGuiSettingsState) => void;
private stateValue: IGuiSettingsState = { ...defaultSettings };
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..09b8d7651f 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());
@@ -192,11 +208,26 @@ export default class WindowController {
private showImmediately() {
const window = this.windowValue;
- this.updatePosition();
- this.notifyUpdateWindowShape();
+ // When running with unpinned window on Windows there's a bug that causes the app to become
+ // wider if opened from minimized if the updated position is set before the window is opened.
+ // Unfortunately the order can't always be changed since this would cause the Window to "jump"
+ // in other scenarios.
+ if (
+ process.platform === 'win32' &&
+ this.windowPositioning instanceof StandaloneWindowPositioning
+ ) {
+ window.show();
+ window.focus();
+
+ this.updatePosition();
+ this.notifyUpdateWindowShape();
+ } else {
+ this.updatePosition();
+ this.notifyUpdateWindowShape();
- window.show();
- window.focus();
+ window.show();
+ window.focus();
+ }
}
private updatePosition() {
diff --git a/gui/src/renderer/app.tsx b/gui/src/renderer/app.tsx
index 17e117f739..d1b2850559 100644
--- a/gui/src/renderer/app.tsx
+++ b/gui/src/renderer/app.tsx
@@ -386,6 +386,10 @@ export default class AppRenderer {
IpcRendererEventChannel.guiSettings.setMonochromaticIcon(monochromaticIcon);
}
+ public setUnpinnedWindow(unpinnedWindow: boolean) {
+ IpcRendererEventChannel.guiSettings.setUnpinnedWindow(unpinnedWindow);
+ }
+
public async verifyWireguardKey(publicKey: IWgKey) {
const actions = this.reduxActions;
actions.settings.verifyWireguardKey(publicKey);
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/components/Preferences.tsx b/gui/src/renderer/components/Preferences.tsx
index a064edcf83..ab2b423686 100644
--- a/gui/src/renderer/components/Preferences.tsx
+++ b/gui/src/renderer/components/Preferences.tsx
@@ -23,7 +23,7 @@ export interface IProps {
enableSystemNotifications: boolean;
monochromaticIcon: boolean;
startMinimized: boolean;
- enableStartMinimizedToggle: boolean;
+ unpinnedWindow: boolean;
setAutoStart: (autoStart: boolean) => void;
setEnableSystemNotifications: (flag: boolean) => void;
setAutoConnect: (autoConnect: boolean) => void;
@@ -31,6 +31,7 @@ export interface IProps {
setShowBetaReleases: (showBetaReleases: boolean) => void;
setStartMinimized: (startMinimized: boolean) => void;
setMonochromaticIcon: (monochromaticIcon: boolean) => void;
+ setUnpinnedWindow: (unpinnedWindow: boolean) => void;
onClose: () => void;
}
@@ -178,7 +179,36 @@ export default class Preferences extends React.Component<IProps> {
</Cell.Footer>
</AriaInputGroup>
- {this.props.enableStartMinimizedToggle ? (
+ {(process.platform === 'win32' ||
+ (process.platform === 'darwin' && process.env.NODE_ENV === 'development')) && (
+ <AriaInputGroup>
+ <Cell.Container>
+ <AriaLabel>
+ <Cell.InputLabel>
+ {messages.pgettext('preferences-view', 'Unpin app from taskbar')}
+ </Cell.InputLabel>
+ </AriaLabel>
+ <AriaInput>
+ <Cell.Switch
+ isOn={this.props.unpinnedWindow}
+ onChange={this.props.setUnpinnedWindow}
+ />
+ </AriaInput>
+ </Cell.Container>
+ <Cell.Footer>
+ <AriaDescription>
+ <Cell.FooterText>
+ {messages.pgettext(
+ 'preferences-view',
+ 'Enable to move the app around as a free-standing window.',
+ )}
+ </Cell.FooterText>
+ </AriaDescription>
+ </Cell.Footer>
+ </AriaInputGroup>
+ )}
+
+ {this.props.unpinnedWindow && (
<React.Fragment>
<AriaInputGroup>
<Cell.Container>
@@ -206,7 +236,7 @@ export default class Preferences extends React.Component<IProps> {
</Cell.Footer>
</AriaInputGroup>
</React.Fragment>
- ) : undefined}
+ )}
<AriaInputGroup>
<Cell.Container disabled={this.props.isBeta}>
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);
diff --git a/gui/src/renderer/containers/PreferencesPage.tsx b/gui/src/renderer/containers/PreferencesPage.tsx
index c7099283f9..35d9c62f5b 100644
--- a/gui/src/renderer/containers/PreferencesPage.tsx
+++ b/gui/src/renderer/containers/PreferencesPage.tsx
@@ -15,6 +15,7 @@ const mapStateToProps = (state: IReduxState) => ({
enableSystemNotifications: state.settings.guiSettings.enableSystemNotifications,
monochromaticIcon: state.settings.guiSettings.monochromaticIcon,
startMinimized: state.settings.guiSettings.startMinimized,
+ unpinnedWindow: state.settings.guiSettings.unpinnedWindow,
});
const mapDispatchToProps = (_dispatch: ReduxDispatch, props: RouteComponentProps & IAppContext) => {
@@ -44,10 +45,12 @@ const mapDispatchToProps = (_dispatch: ReduxDispatch, props: RouteComponentProps
setStartMinimized: (startMinimized: boolean) => {
props.app.setStartMinimized(startMinimized);
},
- enableStartMinimizedToggle: process.platform === 'linux',
setMonochromaticIcon: (monochromaticIcon: boolean) => {
props.app.setMonochromaticIcon(monochromaticIcon);
},
+ setUnpinnedWindow: (unpinnedWindow: boolean) => {
+ props.app.setUnpinnedWindow(unpinnedWindow);
+ },
};
};
diff --git a/gui/src/renderer/redux/settings/reducers.ts b/gui/src/renderer/redux/settings/reducers.ts
index 980eea6cd5..53d19aba29 100644
--- a/gui/src/renderer/redux/settings/reducers.ts
+++ b/gui/src/renderer/redux/settings/reducers.ts
@@ -148,6 +148,7 @@ const initialState: ISettingsReduxState = {
autoConnect: true,
monochromaticIcon: false,
startMinimized: false,
+ unpinnedWindow: process.platform !== 'win32' && process.platform !== 'darwin',
},
relaySettings: {
normal: {
diff --git a/gui/src/shared/gui-settings-state.ts b/gui/src/shared/gui-settings-state.ts
index 043892f833..d8fc1f1ee2 100644
--- a/gui/src/shared/gui-settings-state.ts
+++ b/gui/src/shared/gui-settings-state.ts
@@ -20,4 +20,7 @@ export interface IGuiSettingsState {
// Tells the app to hide the main window on start.
startMinimized: boolean;
+
+ // Tells the app wheter or not it should act as a window or a context menu.
+ unpinnedWindow: boolean;
}
diff --git a/gui/src/shared/ipc-event-channel.ts b/gui/src/shared/ipc-event-channel.ts
index a7410dd22c..587397467c 100644
--- a/gui/src/shared/ipc-event-channel.ts
+++ b/gui/src/shared/ipc-event-channel.ts
@@ -101,6 +101,7 @@ interface IGuiSettingsMethods extends IReceiver<IGuiSettingsState> {
setStartMinimized(startMinimized: boolean): void;
setMonochromaticIcon(monochromaticIcon: boolean): void;
setPreferredLocale(locale: string): void;
+ setUnpinnedWindow(unpinnedWindow: boolean): void;
}
interface IGuiSettingsHandlers extends ISender<IGuiSettingsState> {
@@ -109,6 +110,7 @@ interface IGuiSettingsHandlers extends ISender<IGuiSettingsState> {
handleStartMinimized(fn: (startMinimized: boolean) => void): void;
handleMonochromaticIcon(fn: (monochromaticIcon: boolean) => void): void;
handleSetPreferredLocale(fn: (locale: string) => void): void;
+ handleSetUnpinnedWindow(fn: (unpinnedWindow: boolean) => void): void;
}
interface IAccountHandlers extends ISender<IAccountData | undefined> {
@@ -204,6 +206,7 @@ const SET_AUTO_CONNECT = 'set-auto-connect';
const SET_MONOCHROMATIC_ICON = 'set-monochromatic-icon';
const SET_START_MINIMIZED = 'set-start-minimized';
const SET_PREFERRED_LOCALE = 'set-preferred-locale';
+const SET_UNPINNED_WINDOW = 'set-unpinned-window';
const GET_APP_STATE = 'get-app-state';
@@ -305,6 +308,7 @@ export class IpcRendererEventChannel {
setMonochromaticIcon: set(SET_MONOCHROMATIC_ICON),
setStartMinimized: set(SET_START_MINIMIZED),
setPreferredLocale: set(SET_PREFERRED_LOCALE),
+ setUnpinnedWindow: set(SET_UNPINNED_WINDOW),
};
public static autoStart: IAutoStartMethods = {
@@ -412,6 +416,7 @@ export class IpcMainEventChannel {
handleMonochromaticIcon: handler(SET_MONOCHROMATIC_ICON),
handleStartMinimized: handler(SET_START_MINIMIZED),
handleSetPreferredLocale: handler(SET_PREFERRED_LOCALE),
+ handleSetUnpinnedWindow: handler(SET_UNPINNED_WINDOW),
};
public static autoStart: IAutoStartHandlers = {