diff options
| author | Oskar Nyberg <oskar@mullvad.net> | 2021-10-07 09:44:54 +0200 |
|---|---|---|
| committer | Oskar Nyberg <oskar@mullvad.net> | 2021-10-12 13:38:47 +0200 |
| commit | 9582fb4ad1e5ce11ffa0f1fef3f087e707f5be53 (patch) | |
| tree | 15c06e1ba1c1408da12bb310a4d47b1924c6f561 /gui/src | |
| parent | c94aa2ca013b58f46f64dec7c914a62b0b6cf733 (diff) | |
| download | mullvadvpn-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.ts | 16 | ||||
| -rw-r--r-- | gui/src/main/tray-icon-controller.ts | 137 |
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': |
