fix(system-tray): Move Tray to main process

Summary:
- Fixes #1223
- Still uses a package in the renderer process to listen to changes in
  the unread count and have access to the canvas api.
- Renderer process will write icon to disk and inform main process that
  it should update the icon

Test Plan: - Manual

Reviewers: evan, bengotow

Reviewed By: bengotow

Differential Revision: https://phab.nylas.com/D2613
This commit is contained in:
Juan Tejada 2016-02-23 23:25:04 -08:00
parent 54039db56e
commit 57fb1919ee
7 changed files with 205 additions and 180 deletions

View file

@ -1,30 +1,13 @@
import SystemTray from './system-tray';
import SystemTrayIconStore from './system-tray-icon-store';
const platform = process.platform;
let systemTray;
let unsubConfig = ()=>{};
export function deactivate() {
if (systemTray) {
systemTray.destroy();
systemTray = null;
}
unsubConfig();
export function activate() {
this.store = new SystemTrayIconStore(platform);
this.store.activate();
}
const onSystemTrayToggle = (showSystemTray)=> {
deactivate();
if (showSystemTray.newValue) {
systemTray = new SystemTray(platform);
}
};
export function activate() {
deactivate();
unsubConfig = NylasEnv.config.onDidChange('core.workspace.systemTray', onSystemTrayToggle).dispose;
if (NylasEnv.config.get('core.workspace.systemTray')) {
systemTray = new SystemTray(platform);
}
export function deactivate() {
this.store.deactivate();
}
export function serialize() {

View file

@ -0,0 +1,87 @@
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)
// 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'
)
class SystemTrayIconStore {
constructor(platform) {
this._platform = platform;
this._unreadString = (+UnreadBadgeStore.count()).toLocaleString();
this._unreadIcon = nativeImage.createFromPath(UNREAD_ICON_PATH);
this._baseIcon = nativeImage.createFromPath(BASE_ICON_PATH);
this._icon = this._getIconImg();
}
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);
})
});
}
_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']();
}
_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)
})
};
deactivate() {
if (this._unsubscribe) this._unsubscribe();
}
}
export default SystemTrayIconStore;

View file

@ -1,50 +0,0 @@
import {remote, ipcRenderer} from 'electron';
import TrayStore from './tray-store';
const Tray = remote.require('tray');
class SystemTray {
constructor(platform) {
this._platform = platform;
this._store = new TrayStore(this._platform);
this._tray = new Tray(this._store.icon());
this._tray.setToolTip(this._store.tooltip());
// Check in case there is no menu for the current platform
const menu = this._store.menu();
if (menu != null) this._tray.setContextMenu(menu);
this._unsubscribe = this._addEventListeners();
}
_addEventListeners() {
this._tray.addListener('click', this._onClicked.bind(this));
const unsubClicked = ()=> this._tray.removeListener('click', this._onClicked);
const unsubStore = this._store.listen(this._onChange.bind(this));
return ()=> {
unsubClicked();
unsubStore();
};
}
_onClicked() {
if (this._platform !== 'darwin') {
ipcRenderer.send('command', 'application:show-main-window');
}
}
_onChange() {
const icon = this._store.icon();
const tooltip = this._store.tooltip();
this._tray.setImage(icon);
this._tray.setToolTip(tooltip);
}
destroy() {
this._tray.destroy();
this._unsubscribe();
}
}
export default SystemTray;

View file

@ -1,107 +0,0 @@
import path from 'path';
import NylasStore from 'nylas-store';
import {remote, ipcRenderer} from 'electron';
import {UnreadBadgeStore, CanvasUtils} from 'nylas-exports';
const NativeImage = remote.require('native-image');
const Menu = remote.require('menu');
const {canvasWithSystemTrayIconAndText} = CanvasUtils;
// 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 _menuTemplate = [
{
label: 'New Message',
click: ()=> ipcRenderer.send('command', 'application:new-message'),
},
{
label: 'Preferences',
click: ()=> ipcRenderer.send('command', 'application:open-preferences'),
},
{
type: 'separator',
},
{
label: 'Quit N1',
click: ()=> ipcRenderer.send('command', 'application:quit'),
},
];
if (process.platform !== 'win32') {
_menuTemplate.unshift({
label: 'Open Inbox',
click: ()=> ipcRenderer.send('command', 'application:show-main-window'),
});
}
const _buildMenu = ()=> {
return Menu.buildFromTemplate(_menuTemplate);
};
class TrayStore extends NylasStore {
constructor(platform) {
super();
this._platform = platform;
this._unreadIcon = NativeImage.createFromPath(UNREAD_ICON_PATH);
this._unreadString = (+UnreadBadgeStore.count()).toLocaleString();
this._baseIcon = NativeImage.createFromPath(BASE_ICON_PATH);
this._menu = _buildMenu();
this._icon = this._getIconImg();
this.listenTo(UnreadBadgeStore, this._onUnreadCountChanged);
}
icon() {
return this._icon;
}
tooltip() {
return `${this._unreadString} unread messages`;
}
menu() {
return this._menu;
}
_getIconImg() {
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 (this._unreadString === '0') {
canvas = canvasWithSystemTrayIconAndText(img, '');
} else {
canvas = canvasWithSystemTrayIconAndText(img, this._unreadString);
}
const pngData = NativeImage.createFromDataURL(canvas.toDataURL()).toPng();
// creating from a buffer allows us to specify that the image is @2x
const out2x = NativeImage.createFromBuffer(pngData, 2);
out2x.setTemplateImage(true);
return out2x;
},
'default': ()=> {
return this._unreadString !== '0' ? this._unreadIcon : this._baseIcon;
},
};
return imgHandlers[this._platform in imgHandlers ? this._platform : 'default']();
}
_onUnreadCountChanged() {
this._unreadString = (+UnreadBadgeStore.count()).toLocaleString();
this._icon = this._getIconImg();
this.trigger();
}
}
export default TrayStore;

View file

@ -1,3 +1,4 @@
SystemTrayManager = require './system-tray-manager'
NylasWindow = require './nylas-window'
WindowManager = require './window-manager'
ApplicationMenu = require './application-menu'
@ -56,6 +57,7 @@ class Application
nylasProtocolHandler: null
resourcePath: null
version: null
systemTrayManager: null
exit: (status) -> app.exit(status)
@ -119,6 +121,8 @@ class Application
@applicationMenu = new ApplicationMenu(@version)
@_databasePhase = 'setup'
@systemTrayManager = new SystemTrayManager(process.platform, @)
@listenForArgumentsFromNewProcess()
@setupJavaScriptArguments()
@handleEvents()
@ -321,6 +325,7 @@ class Application
# Destroy hot windows so that they can't block the app from quitting.
# (Electron will wait for them to finish loading before quitting.)
@windowManager.unregisterAllHotWindows()
@systemTrayManager?.destroy()
# Called after the app has closed all windows.
app.on 'will-quit', =>
@ -338,6 +343,10 @@ class Application
@openUrl(urlToOpen)
event.preventDefault()
# System Tray
ipcMain.on 'update-system-tray', (event, iconPath, unreadString) =>
@systemTrayManager?.setTrayCount(iconPath, unreadString)
ipcMain.on 'set-badge-value', (event, value) =>
app.dock?.setBadge?(value)

View file

@ -0,0 +1,102 @@
import fs from 'fs';
import {Tray, Menu, nativeImage} from 'electron';
function _getMenuTemplate(platform, application) {
const template = [
{
label: 'New Message',
click: ()=> application.emit('application:new-message'),
},
{
label: 'Preferences',
click: ()=> application.emit('application:open-preferences'),
},
{
type: 'separator',
},
{
label: 'Quit N1',
click: ()=> application.emit('application:quit'),
},
];
if (platform !== 'win32') {
template.unshift({
label: 'Open Inbox',
click: ()=> application.emit('application:show-main-window'),
});
}
return template;
}
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) {
return nativeImage.createEmpty();
}
}
class SystemTrayManager {
constructor(platform, application) {
this._platform = platform;
this._application = application;
this._iconPath = null;
this._tray = null;
this._initTray();
this._application.config.onDidChange('core.workspace.systemTray', () => {
this.destroy()
this._initTray()
})
}
_initTray() {
if (this._application.config.get('core.workspace.systemTray') !== false) {
this._tray = new Tray(_getIcon(this._iconPath));
this._tray.setToolTip(_getTooltip());
this._tray.setContextMenu(Menu.buildFromTemplate(_getMenuTemplate(this._platform, this._application)));
this._unsubscribe = ()=> this._tray.removeListener('click', this._onClick);
}
}
_onClick() {
if (this._platform !== 'darwin') {
this._application.emit('application:show-main-window');
}
}
setTrayCount(iconPath, unreadString) {
if (!this._tray) return;
this._iconPath = iconPath;
const icon = _getIcon(this._iconPath);
const tooltip = _getTooltip(unreadString);
this._tray.setImage(icon);
this._tray.setToolTip(tooltip);
}
destroy() {
if (!this._tray) return;
this._unsubscribe();
this._tray.destroy();
}
}
export default SystemTrayManager;

View file

@ -54,6 +54,7 @@ module.exports = ErrorLogger = (function() {
/////////////////////////////////////////////////////////////////////
ErrorLogger.prototype.reportError = function(error, extra) {
var nslog = require('nslog');
if (!error) { error = {stack: ""} }
this._appendLog(error.stack)
if (extra) { this._appendLog(extra) }