feat(tray): Update to new set of icons and behavior for tray (menubar)

Summary:
- See #1698

Add specs

Test Plan: - Unit tests

Reviewers: bengotow, evan, drew

Reviewed By: drew

Differential Revision: https://phab.nylas.com/D2734
This commit is contained in:
Juan Tejada 2016-03-14 17:15:34 -07:00
parent 8e9d7ff73e
commit 32a47bcb7a
12 changed files with 139 additions and 85 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

View file

@ -1,8 +1,7 @@
import SystemTrayIconStore from './system-tray-icon-store'; import SystemTrayIconStore from './system-tray-icon-store';
const platform = process.platform;
export function activate() { export function activate() {
this.store = new SystemTrayIconStore(platform); this.store = new SystemTrayIconStore();
this.store.activate(); this.store.activate();
} }

View file

@ -1,86 +1,67 @@
import fs from 'fs';
import path from 'path'; import path from 'path';
import mkdirp from 'mkdirp'; import {ipcRenderer} from 'electron';
import {remote, ipcRenderer} from 'electron'; import {UnreadBadgeStore} from 'nylas-exports';
import {UnreadBadgeStore, CanvasUtils} from 'nylas-exports';
const {canvasWithSystemTrayIconAndText} = CanvasUtils;
const {nativeImage} = remote
const mkdirpAsync = Promise.promisify(mkdirp)
const writeFile = Promise.promisify(fs.writeFile)
// Must be absolute real system path // Must be absolute real system path
// https://github.com/atom/electron/issues/1299 // https://github.com/atom/electron/issues/1299
const BASE_ICON_PATH = path.join(__dirname, '..', 'assets', process.platform, 'ic-systemtray-nylas.png'); const INBOX_ZERO_ICON = path.join(__dirname, '..', 'assets', 'MenuItem-Inbox-Zero.png');
const UNREAD_ICON_PATH = path.join(__dirname, '..', 'assets', process.platform, 'ic-systemtray-nylas-unread.png'); const INBOX_UNREAD_ICON = path.join(__dirname, '..', 'assets', 'MenuItem-Inbox-Full.png');
const TRAY_ICON_PATH = path.join( const INBOX_UNREAD_ALT_ICON = path.join(__dirname, '..', 'assets', 'MenuItem-Inbox-Full-NewItems.png');
NylasEnv.getConfigDirPath(),
'tray',
'tray-icon.png'
);
class SystemTrayIconStore { class SystemTrayIconStore {
constructor(platform) { static INBOX_ZERO_ICON = INBOX_ZERO_ICON;
this._platform = platform;
this._unreadString = (+UnreadBadgeStore.count()).toLocaleString(); static INBOX_UNREAD_ICON = INBOX_UNREAD_ICON;
this._unreadIcon = nativeImage.createFromPath(UNREAD_ICON_PATH);
this._baseIcon = nativeImage.createFromPath(BASE_ICON_PATH); static INBOX_UNREAD_ALT_ICON = INBOX_UNREAD_ALT_ICON;
this._icon = this._getIconImg();
constructor() {
this._windowBlurred = false;
this._unsubscribers = [];
} }
activate() { activate() {
const iconDir = path.dirname(TRAY_ICON_PATH); this._updateIcon()
mkdirpAsync(iconDir).then(()=> { this._unsubscribers.push(UnreadBadgeStore.listen(this._updateIcon));
writeFile(TRAY_ICON_PATH, this._icon.toPng())
.then(()=> { ipcRenderer.on('browser-window-blur', this._onWindowBlur)
ipcRenderer.send('update-system-tray', TRAY_ICON_PATH, this._unreadString); ipcRenderer.on('browser-window-focus', this._onWindowFocus)
this._unsubscribe = UnreadBadgeStore.listen(this._onUnreadCountChanged); this._unsubscribers.push(() => ipcRenderer.removeListener('browser-window-blur', this._onWindowBlur))
}) this._unsubscribers.push(() => ipcRenderer.removeListener('browser-window-focus', this._onWindowFocus))
});
} }
_getIconImg(unreadString = this._unreadString) { _getIconImageData(unreadCount, isWindowBlurred) {
const imgHandlers = { if (unreadCount === 0) {
'darwin': () => { return {iconPath: INBOX_ZERO_ICON, isTemplateImg: true};
const img = new Image(); }
let canvas = null; return isWindowBlurred ?
{iconPath: INBOX_UNREAD_ALT_ICON, isTemplateImg: false} :
// toDataUrl always returns the @1x image data, so the assets/darwin/ {iconPath: INBOX_UNREAD_ICON, isTemplateImg: true};
// contains an "@2x" image /without/ the @2x extension
img.src = this._baseIcon.toDataURL();
if (unreadString === '0') {
canvas = canvasWithSystemTrayIconAndText(img, '');
} else {
canvas = canvasWithSystemTrayIconAndText(img, unreadString);
}
const pngData = nativeImage.createFromDataURL(canvas.toDataURL()).toPng();
// creating from a buffer allows us to specify that the image is @2x
const outputImg = nativeImage.createFromBuffer(pngData);
return outputImg;
},
'default': () => {
return unreadString !== '0' ? this._unreadIcon : this._baseIcon;
},
};
return imgHandlers[this._platform in imgHandlers ? this._platform : 'default']();
} }
_onUnreadCountChanged = () => { _onWindowBlur = ()=> {
this._unreadString = (+UnreadBadgeStore.count()).toLocaleString(); // Set state to blurred, but don't trigger a change. The icon should only be
this._icon = this._getIconImg(); // updated when the count changes
writeFile(TRAY_ICON_PATH, this._icon.toPng()) this._windowBlurred = true;
.then(() => { };
ipcRenderer.send('update-system-tray', TRAY_ICON_PATH, this._unreadString);
}); _onWindowFocus = ()=> {
// Make sure that as long as the window is focused we never use the alt icon
this._windowBlurred = false;
this._updateIcon();
};
_updateIcon = () => {
const count = UnreadBadgeStore.count()
const unreadString = (+count).toLocaleString();
const {iconPath, isTemplateImg} = this._getIconImageData(count, this._windowBlurred);
ipcRenderer.send('update-system-tray', iconPath, unreadString, isTemplateImg);
}; };
deactivate() { deactivate() {
if (this._unsubscribe) this._unsubscribe(); this._unsubscribers.forEach(unsub => unsub())
} }
} }

View file

@ -0,0 +1,81 @@
import {ipcRenderer} from 'electron';
import {UnreadBadgeStore} from 'nylas-exports';
import SystemTrayIconStore from '../lib/system-tray-icon-store';
const {
INBOX_ZERO_ICON,
INBOX_UNREAD_ICON,
INBOX_UNREAD_ALT_ICON,
} = SystemTrayIconStore;
describe('SystemTrayIconStore', ()=> {
beforeEach(()=> {
spyOn(ipcRenderer, 'send')
this.iconStore = new SystemTrayIconStore()
});
function getCallData() {
const {args} = ipcRenderer.send.calls[0]
return {iconPath: args[1], isTemplateImg: args[3]}
}
describe('_getIconImageData', ()=> {
it('shows inbox zero icon when unread count is 0 and window is focused', ()=> {
const {iconPath, isTemplateImg} = this.iconStore._getIconImageData(0, false)
expect(iconPath).toBe(INBOX_ZERO_ICON)
expect(isTemplateImg).toBe(true)
});
it('shows inbox zero icon when unread count is 0 and window is blurred', ()=> {
const {iconPath, isTemplateImg} = this.iconStore._getIconImageData(0, true)
expect(iconPath).toBe(INBOX_ZERO_ICON)
expect(isTemplateImg).toBe(true)
});
it('shows inbox full icon when unread count > 0 and window is focused', ()=> {
const {iconPath, isTemplateImg} = this.iconStore._getIconImageData(1, false)
expect(iconPath).toBe(INBOX_UNREAD_ICON)
expect(isTemplateImg).toBe(true)
});
it('shows inbox full /alt/ icon when unread count > 0 and window is blurred', ()=> {
const {iconPath, isTemplateImg} = this.iconStore._getIconImageData(1, true)
expect(iconPath).toBe(INBOX_UNREAD_ALT_ICON)
expect(isTemplateImg).toBe(false)
});
});
describe('updating the icon based on focus and blur', ()=> {
it('always shows inbox full icon when the window gets focused', ()=> {
spyOn(UnreadBadgeStore, 'count').andReturn(1)
this.iconStore._onWindowFocus()
const {iconPath} = getCallData()
expect(iconPath).toBe(INBOX_UNREAD_ICON)
});
it('shows inbox full /alt/ icon ONLY when window is currently blurred and unread count changes', ()=> {
this.iconStore._windowBlurred = false
this.iconStore._onWindowBlur()
expect(ipcRenderer.send).not.toHaveBeenCalled()
// UnreadBadgeStore triggers a change
spyOn(UnreadBadgeStore, 'count').andReturn(1)
this.iconStore._updateIcon()
const {iconPath} = getCallData()
expect(iconPath).toBe(INBOX_UNREAD_ALT_ICON)
});
it('does not show inbox full /alt/ icon when window is currently focused and unread count changes', ()=> {
this.iconStore._windowBlurred = false
// UnreadBadgeStore triggers a change
spyOn(UnreadBadgeStore, 'count').andReturn(1)
this.iconStore._updateIcon()
const {iconPath} = getCallData()
expect(iconPath).toBe(INBOX_UNREAD_ICON)
});
});
});

View file

@ -347,8 +347,8 @@ class Application
event.preventDefault() event.preventDefault()
# System Tray # System Tray
ipcMain.on 'update-system-tray', (event, iconPath, unreadString) => ipcMain.on 'update-system-tray', (event, args...) =>
@systemTrayManager?.setTrayCount(iconPath, unreadString) @systemTrayManager?.updateTray(args...)
ipcMain.on 'set-badge-value', (event, value) => ipcMain.on 'set-badge-value', (event, value) =>
app.dock?.setBadge?(value) app.dock?.setBadge?(value)

View file

@ -1,4 +1,3 @@
import fs from 'fs';
import {Tray, Menu, nativeImage} from 'electron'; import {Tray, Menu, nativeImage} from 'electron';
@ -35,20 +34,15 @@ function _getTooltip(unreadString) {
return unreadString ? '' : `${unreadString} unread messages`; return unreadString ? '' : `${unreadString} unread messages`;
} }
function _getIcon(iconPath) { function _getIcon(iconPath, isTemplateImg) {
if (!iconPath) return nativeImage.createEmpty() if (!iconPath) {
try {
fs.accessSync(iconPath, fs.F_OK | fs.R_OK)
const buffer = fs.readFileSync(iconPath);
if (buffer.length > 0) {
const out2x = nativeImage.createFromBuffer(buffer, 2);
out2x.setTemplateImage(true);
return out2x;
}
return nativeImage.createEmpty();
} catch (e) {
return nativeImage.createEmpty(); return nativeImage.createEmpty();
} }
const icon = nativeImage.createFromPath(iconPath)
if (isTemplateImg) {
icon.setTemplateImage(true);
}
return icon;
} }
@ -83,20 +77,19 @@ class SystemTrayManager {
} }
}; };
setTrayCount(iconPath, unreadString) { updateTray(iconPath, unreadString, isTemplateImg) {
if (!this._tray) return; if (!this._tray) return;
this._iconPath = iconPath; this._iconPath = iconPath;
const icon = _getIcon(this._iconPath); const icon = _getIcon(this._iconPath, isTemplateImg);
const tooltip = _getTooltip(unreadString); const tooltip = _getTooltip(unreadString);
this._tray.setImage(icon); this._tray.setImage(icon);
this._tray.setToolTip(tooltip); this._tray.setToolTip(tooltip);
} }
destroy() { destroy() {
if (!this._tray) return; if (this._tray) this._tray.destroy();
this._unsubscribe(); this._unsubscribe();
this._tray.destroy();
} }
} }