Electron: Hide to tray on close in Windows (#3828)
This commit is contained in:
parent
1f10dca00e
commit
af69e04e31
@ -17,6 +17,7 @@ src/lib/secret-sauce/
|
||||
playwright.config.ts
|
||||
|
||||
dist
|
||||
dist-electron
|
||||
public
|
||||
|
||||
deploy/update_version.js
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
103
src/electron/tray.ts
Normal 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;
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user