summaryrefslogtreecommitdiffhomepage
path: root/gui/src
diff options
context:
space:
mode:
authorOskar Nyberg <oskar@mullvad.net>2021-10-07 09:44:54 +0200
committerOskar Nyberg <oskar@mullvad.net>2021-10-12 13:38:47 +0200
commit9582fb4ad1e5ce11ffa0f1fef3f087e707f5be53 (patch)
tree15c06e1ba1c1408da12bb310a4d47b1924c6f561 /gui/src
parentc94aa2ca013b58f46f64dec7c914a62b0b6cf733 (diff)
downloadmullvadvpn-9582fb4ad1e5ce11ffa0f1fef3f087e707f5be53.tar.xz
mullvadvpn-9582fb4ad1e5ce11ffa0f1fef3f087e707f5be53.zip
Detect tray color and use appropriate icon color
Diffstat (limited to 'gui/src')
-rw-r--r--gui/src/main/index.ts16
-rw-r--r--gui/src/main/tray-icon-controller.ts137
2 files changed, 129 insertions, 24 deletions
diff --git a/gui/src/main/index.ts b/gui/src/main/index.ts
index ac6faf2df2..4bd963f1b8 100644
--- a/gui/src/main/index.ts
+++ b/gui/src/main/index.ts
@@ -6,6 +6,7 @@ import {
dialog,
Menu,
nativeImage,
+ nativeTheme,
screen,
session,
shell,
@@ -463,12 +464,21 @@ class ApplicationMain {
const tray = this.createTray();
const windowController = new WindowController(window, tray, this.guiSettings.unpinnedWindow);
- this.tunnelStateExpectation = new Expectation(() => {
+ this.tunnelStateExpectation = new Expectation(async () => {
this.trayIconController = new TrayIconController(
tray,
this.trayIconType(this.tunnelState, this.settings.blockWhenDisconnected),
this.guiSettings.monochromaticIcon,
);
+ await this.trayIconController.updateTheme();
+
+ if (process.platform === 'win32') {
+ nativeTheme.on('updated', async () => {
+ if (this.guiSettings.monochromaticIcon) {
+ await this.trayIconController?.updateTheme();
+ }
+ });
+ }
});
this.registerIpcListeners();
@@ -476,10 +486,10 @@ class ApplicationMain {
this.windowController = windowController;
this.tray = tray;
- this.guiSettings.onChange = (newState, oldState) => {
+ this.guiSettings.onChange = async (newState, oldState) => {
if (oldState.monochromaticIcon !== newState.monochromaticIcon) {
if (this.trayIconController) {
- this.trayIconController.useMonochromaticIcon = newState.monochromaticIcon;
+ await this.trayIconController.setUseMonochromaticIcon(newState.monochromaticIcon);
}
}
diff --git a/gui/src/main/tray-icon-controller.ts b/gui/src/main/tray-icon-controller.ts
index bb1d4f5868..a16c2ad9fc 100644
--- a/gui/src/main/tray-icon-controller.ts
+++ b/gui/src/main/tray-icon-controller.ts
@@ -1,27 +1,32 @@
+import { exec as execAsync } from 'child_process';
import { nativeImage, NativeImage, Tray } from 'electron';
import path from 'path';
+import { promisify } from 'util';
+import log from '../shared/logging';
import KeyframeAnimation from './keyframe-animation';
+const exec = promisify(execAsync);
+
export type TrayIconType = 'unsecured' | 'securing' | 'secured';
+type IconSets = {
+ regular: NativeImage[];
+ template: NativeImage[];
+ white: NativeImage[];
+ black: NativeImage[];
+};
+
export default class TrayIconController {
private animation?: KeyframeAnimation;
- private iconImages: NativeImage[] = [];
+ private iconSets: IconSets = { regular: [], template: [], white: [], black: [] };
+ private iconSet: NativeImage[] = [];
constructor(
- tray: Tray,
+ private tray: Tray,
private iconTypeValue: TrayIconType,
private useMonochromaticIconValue: boolean,
) {
this.loadImages();
-
- const initialFrame = this.targetFrame();
- const animation = new KeyframeAnimation();
- animation.speed = 100;
- animation.onFrame = (frameNumber) => tray.setImage(this.iconImages[frameNumber]);
- animation.play({ start: initialFrame, end: initialFrame });
-
- this.animation = animation;
}
public dispose() {
@@ -35,15 +40,41 @@ export default class TrayIconController {
return this.iconTypeValue;
}
- set useMonochromaticIcon(useMonochromaticIcon: boolean) {
- this.useMonochromaticIconValue = useMonochromaticIcon;
- this.loadImages();
+ public async updateTheme() {
+ if (this.useMonochromaticIconValue) {
+ switch (process.platform) {
+ case 'darwin':
+ this.iconSet = this.iconSets.template;
+ break;
+ case 'win32': {
+ if (await this.getSystemUsesLightTheme()) {
+ this.iconSet = this.iconSets.black;
+ } else {
+ this.iconSet = this.iconSets.white;
+ }
+ break;
+ }
+ case 'linux':
+ default:
+ this.iconSet = this.iconSets.white;
+ break;
+ }
+ } else {
+ this.iconSet = this.iconSets.regular;
+ }
- if (this.animation && !this.animation.isRunning) {
+ if (this.animation === undefined) {
+ this.initAnimation();
+ } else if (!this.animation.isRunning) {
this.animation.play({ end: this.targetFrame() });
}
}
+ public async setUseMonochromaticIcon(useMonochromaticIcon: boolean) {
+ this.useMonochromaticIconValue = useMonochromaticIcon;
+ await this.updateTheme();
+ }
+
public animateToIcon(type: TrayIconType) {
if (this.iconTypeValue === type || !this.animation) {
return;
@@ -57,22 +88,86 @@ export default class TrayIconController {
animation.play({ end: frame });
}
+ private initAnimation() {
+ const initialFrame = this.targetFrame();
+ const animation = new KeyframeAnimation();
+ animation.speed = 100;
+ animation.onFrame = this.onFrame;
+ animation.play({ start: initialFrame, end: initialFrame });
+
+ this.animation = animation;
+ }
+
+ private onFrame = (frameNumber: number) => {
+ const frame = this.iconSet[frameNumber];
+ if (frame === undefined) {
+ log.error('Failed to show tray icon due to the icon being undefined');
+ } else {
+ this.tray.setImage(frame);
+ }
+ };
+
private loadImages() {
+ this.iconSets.regular = this.loadImageSet('');
+
+ switch (process.platform) {
+ case 'darwin':
+ this.iconSets.template = this.loadImageSet('Template');
+ break;
+ case 'win32':
+ this.iconSets.white = this.loadImageSet('_white');
+ this.iconSets.black = this.loadImageSet('_black');
+ break;
+ case 'linux':
+ default:
+ this.iconSets.white = this.loadImageSet('_white');
+ break;
+ }
+ }
+
+ private loadImageSet(suffix: string): NativeImage[] {
const frames = Array.from({ length: 10 }, (_, i) => i + 1);
- this.iconImages = frames.map((frame) => nativeImage.createFromPath(this.getImagePath(frame)));
+ return frames.map((frame) => nativeImage.createFromPath(this.getImagePath(frame, suffix)));
}
- private getImagePath(frame: number) {
+ private getImagePath(frame: number, suffix?: string) {
const basePath = path.resolve(path.join(__dirname, '../../assets/images/menubar icons'));
const extension = process.platform === 'win32' ? 'ico' : 'png';
- let suffix = '';
- if (this.useMonochromaticIconValue) {
- suffix = process.platform === 'darwin' ? 'Template' : '_white';
- }
-
return path.join(basePath, process.platform, `lock-${frame}${suffix}.${extension}`);
}
+ private async getSystemUsesLightTheme(): Promise<boolean | undefined> {
+ try {
+ // This registry entry contains information about the tray background color. This is
+ // needed to decide between white and black icons.
+ const { stdout, stderr } = await exec(
+ 'reg query HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize\\ /v SystemUsesLightTheme',
+ );
+
+ if (!stderr && stdout) {
+ // Split the output into rows
+ const rows = stdout.split('\n');
+ // Select the row that contains the registry entry result
+ const resultRow = rows.find((row) => row.includes('SystemUsesLightTheme'))?.trim();
+ // Split the row into words
+ const resultRowWords = resultRow?.split(' ').filter((word) => word !== '');
+ // Grab value which is last word on the result row
+ const value = resultRowWords && resultRowWords[resultRowWords.length - 1];
+
+ if (value) {
+ const parsedValue = parseInt(value);
+ return parsedValue === 1 ? true : false;
+ }
+ }
+
+ return undefined;
+ } catch (e) {
+ const error = e as Error;
+ log.error('Failed to read SystemUsesLightTheme,', error.message);
+ return undefined;
+ }
+ }
+
private targetFrame(): number {
switch (this.iconTypeValue) {
case 'unsecured':