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';
const platform = process.platform;
export function activate() {
this.store = new SystemTrayIconStore(platform);
this.store = new SystemTrayIconStore();
this.store.activate();
}

View file

@ -1,86 +1,67 @@
import fs from 'fs';
import path from 'path';
import mkdirp from 'mkdirp';
import {remote, ipcRenderer} from 'electron';
import {UnreadBadgeStore, CanvasUtils} from 'nylas-exports';
const {canvasWithSystemTrayIconAndText} = CanvasUtils;
const {nativeImage} = remote
const mkdirpAsync = Promise.promisify(mkdirp)
const writeFile = Promise.promisify(fs.writeFile)
import {ipcRenderer} from 'electron';
import {UnreadBadgeStore} from 'nylas-exports';
// Must be absolute real system path
// https://github.com/atom/electron/issues/1299
const BASE_ICON_PATH = path.join(__dirname, '..', 'assets', process.platform, 'ic-systemtray-nylas.png');
const UNREAD_ICON_PATH = path.join(__dirname, '..', 'assets', process.platform, 'ic-systemtray-nylas-unread.png');
const TRAY_ICON_PATH = path.join(
NylasEnv.getConfigDirPath(),
'tray',
'tray-icon.png'
);
const INBOX_ZERO_ICON = path.join(__dirname, '..', 'assets', 'MenuItem-Inbox-Zero.png');
const INBOX_UNREAD_ICON = path.join(__dirname, '..', 'assets', 'MenuItem-Inbox-Full.png');
const INBOX_UNREAD_ALT_ICON = path.join(__dirname, '..', 'assets', 'MenuItem-Inbox-Full-NewItems.png');
class SystemTrayIconStore {
constructor(platform) {
this._platform = platform;
static INBOX_ZERO_ICON = INBOX_ZERO_ICON;
this._unreadString = (+UnreadBadgeStore.count()).toLocaleString();
this._unreadIcon = nativeImage.createFromPath(UNREAD_ICON_PATH);
this._baseIcon = nativeImage.createFromPath(BASE_ICON_PATH);
this._icon = this._getIconImg();
static INBOX_UNREAD_ICON = INBOX_UNREAD_ICON;
static INBOX_UNREAD_ALT_ICON = INBOX_UNREAD_ALT_ICON;
constructor() {
this._windowBlurred = false;
this._unsubscribers = [];
}
activate() {
const iconDir = path.dirname(TRAY_ICON_PATH);
mkdirpAsync(iconDir).then(()=> {
writeFile(TRAY_ICON_PATH, this._icon.toPng())
.then(()=> {
ipcRenderer.send('update-system-tray', TRAY_ICON_PATH, this._unreadString);
this._unsubscribe = UnreadBadgeStore.listen(this._onUnreadCountChanged);
})
});
this._updateIcon()
this._unsubscribers.push(UnreadBadgeStore.listen(this._updateIcon));
ipcRenderer.on('browser-window-blur', this._onWindowBlur)
ipcRenderer.on('browser-window-focus', this._onWindowFocus)
this._unsubscribers.push(() => ipcRenderer.removeListener('browser-window-blur', this._onWindowBlur))
this._unsubscribers.push(() => ipcRenderer.removeListener('browser-window-focus', this._onWindowFocus))
}
_getIconImg(unreadString = this._unreadString) {
const imgHandlers = {
'darwin': () => {
const img = new Image();
let canvas = null;
// toDataUrl always returns the @1x image data, so the assets/darwin/
// 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']();
_getIconImageData(unreadCount, isWindowBlurred) {
if (unreadCount === 0) {
return {iconPath: INBOX_ZERO_ICON, isTemplateImg: true};
}
return isWindowBlurred ?
{iconPath: INBOX_UNREAD_ALT_ICON, isTemplateImg: false} :
{iconPath: INBOX_UNREAD_ICON, isTemplateImg: true};
}
_onUnreadCountChanged = () => {
this._unreadString = (+UnreadBadgeStore.count()).toLocaleString();
this._icon = this._getIconImg();
writeFile(TRAY_ICON_PATH, this._icon.toPng())
.then(() => {
ipcRenderer.send('update-system-tray', TRAY_ICON_PATH, this._unreadString);
});
_onWindowBlur = ()=> {
// Set state to blurred, but don't trigger a change. The icon should only be
// updated when the count changes
this._windowBlurred = true;
};
_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() {
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()
# System Tray
ipcMain.on 'update-system-tray', (event, iconPath, unreadString) =>
@systemTrayManager?.setTrayCount(iconPath, unreadString)
ipcMain.on 'update-system-tray', (event, args...) =>
@systemTrayManager?.updateTray(args...)
ipcMain.on 'set-badge-value', (event, value) =>
app.dock?.setBadge?(value)

View file

@ -1,4 +1,3 @@
import fs from 'fs';
import {Tray, Menu, nativeImage} from 'electron';
@ -35,20 +34,15 @@ function _getTooltip(unreadString) {
return unreadString ? '' : `${unreadString} unread messages`;
}
function _getIcon(iconPath) {
if (!iconPath) return nativeImage.createEmpty()
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) {
function _getIcon(iconPath, isTemplateImg) {
if (!iconPath) {
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;
this._iconPath = iconPath;
const icon = _getIcon(this._iconPath);
const icon = _getIcon(this._iconPath, isTemplateImg);
const tooltip = _getTooltip(unreadString);
this._tray.setImage(icon);
this._tray.setToolTip(tooltip);
}
destroy() {
if (!this._tray) return;
if (this._tray) this._tray.destroy();
this._unsubscribe();
this._tray.destroy();
}
}