Electron: Hide to tray on close in Windows (#3828)

This commit is contained in:
Alexander Zinchuk 2023-09-25 12:59:50 +02:00
parent 1f10dca00e
commit af69e04e31
10 changed files with 191 additions and 41 deletions

View File

@ -17,6 +17,7 @@ src/lib/secret-sauce/
playwright.config.ts
dist
dist-electron
public
deploy/update_version.js

View File

@ -1,7 +1,6 @@
import type { FC } from '../../../lib/teact/teact';
import React, {
memo,
useCallback,
memo, useCallback, useEffect, useState,
} from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
@ -9,17 +8,19 @@ import type { ISettings, TimeFormat } from '../../../types';
import type { IRadioOption } from '../../ui/RadioGroup';
import { SettingsScreens } from '../../../types';
import { IS_ELECTRON } from '../../../config';
import { pick } from '../../../util/iteratees';
import { setTimeFormat } from '../../../util/langProvider';
import { getSystemTheme } from '../../../util/systemTheme';
import {
IS_ANDROID, IS_IOS, IS_MAC_OS,
IS_ANDROID, IS_IOS, IS_MAC_OS, IS_WINDOWS,
} from '../../../util/windowEnvironment';
import useAppLayout from '../../../hooks/useAppLayout';
import useHistoryBack from '../../../hooks/useHistoryBack';
import useLang from '../../../hooks/useLang';
import Checkbox from '../../ui/Checkbox';
import ListItem from '../../ui/ListItem';
import RadioGroup from '../../ui/RadioGroup';
import RangeSlider from '../../ui/RangeSlider';
@ -117,6 +118,15 @@ const SettingsGeneral: FC<OwnProps & StateProps> = ({
setSettingOption({ messageSendKeyCombo: newCombo as ISettings['messageSendKeyCombo'] });
}, [setSettingOption]);
const [isTrayIconEnabled, setIsTrayIconEnabled] = useState(false);
useEffect(() => {
window.electron?.getIsTrayIconEnabled().then(setIsTrayIconEnabled);
}, []);
const handleIsTrayIconEnabledChange = useCallback((isChecked: boolean) => {
window.electron?.setIsTrayIconEnabled(isChecked);
}, []);
useHistoryBack({
isActive,
onBack: onReset,
@ -142,6 +152,14 @@ const SettingsGeneral: FC<OwnProps & StateProps> = ({
>
{lang('ChatBackground')}
</ListItem>
{IS_ELECTRON && IS_WINDOWS && (
<Checkbox
label={lang('GeneralSettings.StatusBarItem')}
checked={Boolean(isTrayIconEnabled)}
onCheck={handleIsTrayIconEnabledChange}
/>
)}
</div>
<div className="settings-item">

View File

@ -7,6 +7,7 @@ extraMetadata:
files:
- "dist"
- "package.json"
- "public/icon-electron-windows.ico"
- "!dist/**/build-stats.json"
- "!dist/**/statoscope-report.html"
- "!dist/**/reference.json"

View File

@ -4,7 +4,7 @@ import path from 'path';
import { ElectronEvent } from '../types/electron';
import {
IS_LINUX, IS_MAC_OS, IS_WINDOWS, windows,
getLastWindow, IS_LINUX, IS_MAC_OS, IS_WINDOWS,
} from './utils';
const TG_PROTOCOL = 'tg';
@ -12,7 +12,7 @@ const TG_PROTOCOL = 'tg';
let deeplinkUrl: string | undefined;
export function initDeeplink() {
const currentWindow = Array.from(windows).pop();
const window = getLastWindow();
if (process.defaultApp) {
if (process.argv.length >= 2) {
@ -51,24 +51,24 @@ export function initDeeplink() {
processDeeplink();
if (currentWindow) {
if (currentWindow.isMinimized()) {
currentWindow.restore();
if (window) {
if (window.isMinimized()) {
window.restore();
}
currentWindow.focus();
window.focus();
}
});
}
export function processDeeplink() {
const currentWindow = Array.from(windows).pop();
const window = getLastWindow();
if (!currentWindow || !deeplinkUrl) {
if (!window || !deeplinkUrl) {
return;
}
currentWindow.webContents.send(ElectronEvent.DEEPLINK, deeplinkUrl);
window.webContents.send(ElectronEvent.DEEPLINK, deeplinkUrl);
deeplinkUrl = undefined;
}

View File

@ -12,6 +12,8 @@ const electronApi: ElectronApi = {
setWindowTitle: (title?: string) => ipcRenderer.invoke(ElectronAction.SET_WINDOW_TITLE, title),
setTrafficLightPosition:
(position: TrafficLightPosition) => ipcRenderer.invoke(ElectronAction.SET_TRAFFIC_LIGHT_POSITION, position),
setIsTrayIconEnabled: (value: boolean) => ipcRenderer.invoke(ElectronAction.SET_IS_TRAY_ICON_ENABLED, value),
getIsTrayIconEnabled: () => ipcRenderer.invoke(ElectronAction.GET_IS_TRAY_ICON_ENABLED),
on: (eventName: ElectronEvent, callback) => {
const subscription = (event: IpcRendererEvent, ...args: any) => callback(...args);

103
src/electron/tray.ts Normal file
View File

@ -0,0 +1,103 @@
import {
app, BrowserWindow, Menu, nativeImage, Tray,
} from 'electron';
import path from 'path';
import {
forceQuit, getAppTitle, getLastWindow, store,
} from './utils';
const TRAY_ICON_SETTINGS_KEY = 'trayIcon';
const WINDOW_BLUR_TIMEOUT = 800;
interface TrayHelper {
instance?: Tray;
lastFocusedWindow?: BrowserWindow;
lastFocusedWindowTimer?: NodeJS.Timer;
setupListeners: (window: BrowserWindow) => void;
create: () => void;
enable: () => void;
disable: () => void;
isEnabled: boolean;
}
const tray: TrayHelper = {
setupListeners(window: BrowserWindow) {
window.on('focus', () => {
clearTimeout(this.lastFocusedWindowTimer);
this.lastFocusedWindow = window;
});
window.on('blur', () => {
this.lastFocusedWindowTimer = setTimeout(() => {
if (this.lastFocusedWindow === window) {
this.lastFocusedWindow = undefined;
}
}, WINDOW_BLUR_TIMEOUT);
});
window.on('close', () => {
this.lastFocusedWindow = undefined;
});
},
create() {
if (this.instance) {
return;
}
const icon = nativeImage.createFromPath(path.resolve(__dirname, '../public/icon-electron-windows.ico'));
const title = getAppTitle();
this.instance = new Tray(icon);
const handleOpenFromTray = () => {
if (BrowserWindow.getAllWindows().every((window) => !window.isVisible())) {
BrowserWindow.getAllWindows().forEach((window) => window.show());
} else {
getLastWindow()?.focus();
}
};
const handleCloseFromTray = () => {
forceQuit.enable();
app.quit();
};
const handleTrayClick = () => {
if (this.lastFocusedWindow) {
BrowserWindow.getAllWindows().forEach((window) => window.hide());
this.lastFocusedWindow = undefined;
} else {
handleOpenFromTray();
}
};
const contextMenu = Menu.buildFromTemplate([
{ label: `Open ${title}`, click: handleOpenFromTray },
{ label: `Quit ${title}`, click: handleCloseFromTray },
]);
this.instance.on('click', handleTrayClick);
this.instance.setContextMenu(contextMenu);
this.instance.setToolTip(title);
this.instance.setTitle(title);
},
enable() {
store.set(TRAY_ICON_SETTINGS_KEY, true);
this.create();
},
disable() {
store.set(TRAY_ICON_SETTINGS_KEY, false);
this.instance?.destroy();
this.instance = undefined;
},
get isEnabled(): boolean {
return store.get(TRAY_ICON_SETTINGS_KEY, true) as boolean;
},
};
export default tray;

View File

@ -1,14 +1,28 @@
import type { BrowserWindow, Point } from 'electron';
import { app } from 'electron';
import type { Point } from 'electron';
import { app, BrowserWindow } from 'electron';
import Store from 'electron-store';
import type { TrafficLightPosition } from '../types/electron';
export const windows = new Set<BrowserWindow>();
export const IS_MAC_OS = process.platform === 'darwin';
export const IS_WINDOWS = process.platform === 'win32';
export const IS_LINUX = process.platform === 'linux';
export const windows = new Set<BrowserWindow>();
export const store: Store = new Store();
export function getCurrentWindow(): BrowserWindow | null {
return BrowserWindow.getFocusedWindow();
}
export function getLastWindow(): BrowserWindow | undefined {
return Array.from(windows).pop();
}
export function hasExtraWindows(): boolean {
return BrowserWindow.getAllWindows().length > 1;
}
export function getAppTitle(chatTitle?: string): string {
const appName = app.getName();

View File

@ -9,8 +9,10 @@ import { ElectronAction, ElectronEvent } from '../types/electron';
import setupAutoUpdates from './autoUpdates';
import { processDeeplink } from './deeplink';
import tray from './tray';
import {
forceQuit, getAppTitle, IS_MAC_OS, TRAFFIC_LIGHT_POSITION, windows,
forceQuit, getAppTitle, getCurrentWindow, getLastWindow, hasExtraWindows, IS_MAC_OS, IS_WINDOWS,
TRAFFIC_LIGHT_POSITION, windows,
} from './utils';
import windowStateKeeper from './windowState';
@ -25,7 +27,7 @@ export function createWindow(url?: string) {
let x;
let y;
const currentWindow = BrowserWindow.getFocusedWindow();
const currentWindow = getCurrentWindow();
if (currentWindow) {
const [currentWindowX, currentWindowY] = currentWindow.getPosition();
x = currentWindowX + 24;
@ -90,20 +92,16 @@ export function createWindow(url?: string) {
});
window.on('close', (event) => {
if (IS_MAC_OS) {
if (IS_MAC_OS || IS_WINDOWS) {
if (forceQuit.isEnabled) {
app.exit(0);
forceQuit.disable();
} else if (hasExtraWindows()) {
windows.delete(window);
windowState.unmanage();
} else {
const hasExtraWindows = BrowserWindow.getAllWindows().length > 1;
if (hasExtraWindows) {
windows.delete(window);
windowState.unmanage();
} else {
event.preventDefault();
window.hide();
}
event.preventDefault();
window.hide();
}
}
});
@ -123,6 +121,11 @@ export function createWindow(url?: string) {
window.removeMenu();
}
if (IS_WINDOWS && tray.isEnabled) {
tray.setupListeners(window);
tray.create();
}
window.webContents.once('dom-ready', () => {
window.show();
processDeeplink();
@ -141,17 +144,15 @@ export function setupElectronActionHandlers() {
});
ipcMain.handle(ElectronAction.SET_WINDOW_TITLE, (_, newTitle?: string) => {
const currentWindow = BrowserWindow.getFocusedWindow();
currentWindow?.setTitle(getAppTitle(newTitle));
getCurrentWindow()?.setTitle(getAppTitle(newTitle));
});
ipcMain.handle(ElectronAction.GET_IS_FULLSCREEN, () => {
const currentWindow = BrowserWindow.getFocusedWindow();
currentWindow?.isFullScreen();
getCurrentWindow()?.isFullScreen();
});
ipcMain.handle(ElectronAction.HANDLE_DOUBLE_CLICK, () => {
const currentWindow = BrowserWindow.getFocusedWindow();
const currentWindow = getCurrentWindow();
const doubleClickAction = systemPreferences.getUserDefault('AppleActionOnDoubleClick', 'string');
if (doubleClickAction === 'Minimize') {
@ -170,9 +171,18 @@ export function setupElectronActionHandlers() {
return;
}
const currentWindow = BrowserWindow.getFocusedWindow();
currentWindow?.setTrafficLightPosition(TRAFFIC_LIGHT_POSITION[position]);
getCurrentWindow()?.setTrafficLightPosition(TRAFFIC_LIGHT_POSITION[position]);
});
ipcMain.handle(ElectronAction.SET_IS_TRAY_ICON_ENABLED, (_, value: boolean) => {
if (value) {
tray.enable();
} else {
tray.disable();
}
});
ipcMain.handle(ElectronAction.GET_IS_TRAY_ICON_ENABLED, () => tray.isEnabled);
}
export function setupCloseHandlers() {
@ -197,9 +207,7 @@ export function setupCloseHandlers() {
createWindow();
} else if (IS_MAC_OS) {
forceQuit.disable();
const currentWindow = Array.from(windows).pop();
currentWindow?.show();
getLastWindow()?.show();
}
});
}

View File

@ -1,6 +1,7 @@
import type { BrowserWindow, Rectangle } from 'electron';
import { screen } from 'electron';
import Store from 'electron-store';
import { store } from './utils';
type Options = {
defaultHeight?: number;
@ -40,8 +41,6 @@ const DEFAULT_OPTIONS = {
fullScreen: true,
};
const store: Store = new Store();
function windowStateKeeper(options: Options): WindowState {
let state: State;
let winRef: BrowserWindow | undefined;

View File

@ -12,6 +12,8 @@ export enum ElectronAction {
OPEN_NEW_WINDOW = 'open-new-window',
SET_WINDOW_TITLE = 'set-window-title',
SET_TRAFFIC_LIGHT_POSITION = 'set-traffic-light-position',
SET_IS_TRAY_ICON_ENABLED = 'set-is-tray-icon-enabled',
GET_IS_TRAY_ICON_ENABLED = 'get-is-tray-icon-enabled',
}
export type TrafficLightPosition = 'standard' | 'lowered';
@ -23,6 +25,8 @@ export interface ElectronApi {
openNewWindow: (url: string, title?: string) => Promise<void>;
setWindowTitle: (title?: string) => Promise<void>;
setTrafficLightPosition: (position: TrafficLightPosition) => Promise<void>;
setIsTrayIconEnabled: (value: boolean) => Promise<void>;
getIsTrayIconEnabled: () => Promise<boolean>;
on: (eventName: ElectronEvent, callback: any) => VoidFunction;
}