Bullet-proof tray icon against nonexistent icon file
This commit is contained in:
parent
99b19d4b80
commit
940f009987
2 changed files with 49 additions and 6 deletions
|
@ -2,7 +2,14 @@
|
||||||
// SPDX-License-Identifier: AGPL-3.0-only
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { BrowserWindow, app, Menu, Tray } from 'electron';
|
import {
|
||||||
|
BrowserWindow,
|
||||||
|
Menu,
|
||||||
|
NativeImage,
|
||||||
|
Tray,
|
||||||
|
app,
|
||||||
|
nativeImage,
|
||||||
|
} from 'electron';
|
||||||
import * as log from '../ts/logging/log';
|
import * as log from '../ts/logging/log';
|
||||||
import type { LocaleMessagesType } from '../ts/types/I18N';
|
import type { LocaleMessagesType } from '../ts/types/I18N';
|
||||||
|
|
||||||
|
@ -105,7 +112,14 @@ export class SystemTrayService {
|
||||||
this.tray = this.tray || this.createTray();
|
this.tray = this.tray || this.createTray();
|
||||||
const { browserWindow, tray } = this;
|
const { browserWindow, tray } = this;
|
||||||
|
|
||||||
|
try {
|
||||||
tray.setImage(getIcon(this.unreadCount));
|
tray.setImage(getIcon(this.unreadCount));
|
||||||
|
} catch (err: unknown) {
|
||||||
|
log.warn(
|
||||||
|
'System tray service: failed to set preferred image. Falling back...'
|
||||||
|
);
|
||||||
|
tray.setImage(getDefaultIcon());
|
||||||
|
}
|
||||||
|
|
||||||
// NOTE: we want to have the show/hide entry available in the tray icon
|
// NOTE: we want to have the show/hide entry available in the tray icon
|
||||||
// context menu, since the 'click' event may not work on all platforms.
|
// context menu, since the 'click' event may not work on all platforms.
|
||||||
|
@ -169,7 +183,7 @@ export class SystemTrayService {
|
||||||
log.info('System tray service: creating the tray');
|
log.info('System tray service: creating the tray');
|
||||||
|
|
||||||
// This icon may be swiftly overwritten.
|
// This icon may be swiftly overwritten.
|
||||||
const result = new Tray(getIcon(this.unreadCount));
|
const result = new Tray(getDefaultIcon());
|
||||||
|
|
||||||
// Note: "When app indicator is used on Linux, the click event is ignored." This
|
// Note: "When app indicator is used on Linux, the click event is ignored." This
|
||||||
// doesn't mean that the click event is always ignored on Linux; it depends on how
|
// doesn't mean that the click event is always ignored on Linux; it depends on how
|
||||||
|
@ -223,6 +237,12 @@ function getIcon(unreadCount: number) {
|
||||||
return join(__dirname, '..', 'images', `icon_${iconSize}.png`);
|
return join(__dirname, '..', 'images', `icon_${iconSize}.png`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let defaultIcon: undefined | NativeImage;
|
||||||
|
function getDefaultIcon(): NativeImage {
|
||||||
|
defaultIcon ??= nativeImage.createFromPath(getIcon(0));
|
||||||
|
return defaultIcon;
|
||||||
|
}
|
||||||
|
|
||||||
function forceOnTop(browserWindow: BrowserWindow) {
|
function forceOnTop(browserWindow: BrowserWindow) {
|
||||||
// On some versions of GNOME the window may not be on top when restored.
|
// On some versions of GNOME the window may not be on top when restored.
|
||||||
// This trick should fix it.
|
// This trick should fix it.
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
import { assert } from 'chai';
|
import { assert } from 'chai';
|
||||||
import * as sinon from 'sinon';
|
import * as sinon from 'sinon';
|
||||||
import { BrowserWindow, MenuItem, Tray } from 'electron';
|
import { BrowserWindow, MenuItem, Tray, nativeImage } from 'electron';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
||||||
import { SystemTrayService } from '../../../app/SystemTrayService';
|
import { SystemTrayService } from '../../../app/SystemTrayService';
|
||||||
|
@ -208,9 +208,9 @@ describe('SystemTrayService', () => {
|
||||||
// Ideally, there'd be something like `tray.getImage`, but that doesn't exist. We also
|
// Ideally, there'd be something like `tray.getImage`, but that doesn't exist. We also
|
||||||
// can't spy on `Tray.prototype.setImage` because it's not defined that way. So we
|
// can't spy on `Tray.prototype.setImage` because it's not defined that way. So we
|
||||||
// spy on the specific instance, just to get the image.
|
// spy on the specific instance, just to get the image.
|
||||||
const setContextMenuSpy = sandbox.spy(tray, 'setImage');
|
const setImageSpy = sandbox.spy(tray, 'setImage');
|
||||||
const getImagePath = (): string => {
|
const getImagePath = (): string => {
|
||||||
const result = setContextMenuSpy.lastCall?.firstArg;
|
const result = setImageSpy.lastCall?.firstArg;
|
||||||
if (!result) {
|
if (!result) {
|
||||||
throw new Error('Expected tray.setImage to be called at least once');
|
throw new Error('Expected tray.setImage to be called at least once');
|
||||||
}
|
}
|
||||||
|
@ -230,4 +230,27 @@ describe('SystemTrayService', () => {
|
||||||
service.setUnreadCount(0);
|
service.setUnreadCount(0);
|
||||||
assert.match(path.parse(getImagePath()).base, /^icon_\d+\.png$/);
|
assert.match(path.parse(getImagePath()).base, /^icon_\d+\.png$/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('uses a fallback image if the icon file cannot be found', () => {
|
||||||
|
const service = newService();
|
||||||
|
service.setEnabled(true);
|
||||||
|
service.setMainWindow(new BrowserWindow({ show: false }));
|
||||||
|
|
||||||
|
const tray = service._getTray();
|
||||||
|
if (!tray) {
|
||||||
|
throw new Error('Test setup failed: expected a tray');
|
||||||
|
}
|
||||||
|
|
||||||
|
const setImageStub = sandbox.stub(tray, 'setImage');
|
||||||
|
setImageStub.withArgs(sinon.match.string).throws('Failed to load');
|
||||||
|
|
||||||
|
service.setUnreadCount(4);
|
||||||
|
|
||||||
|
// Electron doesn't export this class, so we have to wrestle it out.
|
||||||
|
const NativeImage = nativeImage.createEmpty().constructor;
|
||||||
|
|
||||||
|
sinon.assert.calledTwice(setImageStub);
|
||||||
|
sinon.assert.calledWith(setImageStub, sinon.match.string);
|
||||||
|
sinon.assert.calledWith(setImageStub, sinon.match.instanceOf(NativeImage));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Add table
Reference in a new issue