From 57fb1919ee71dbaf3acf7e78f9ca7f7cd860b3dd Mon Sep 17 00:00:00 2001 From: Juan Tejada Date: Tue, 23 Feb 2016 23:25:04 -0800 Subject: [PATCH] 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 --- internal_packages/system-tray/lib/main.es6 | 29 +---- .../lib/system-tray-icon-store.es6 | 87 ++++++++++++++ .../system-tray/lib/system-tray.es6 | 50 -------- .../system-tray/lib/tray-store.es6 | 107 ------------------ src/browser/application.coffee | 9 ++ src/browser/system-tray-manager.es6 | 102 +++++++++++++++++ src/error-logger.js | 1 + 7 files changed, 205 insertions(+), 180 deletions(-) create mode 100644 internal_packages/system-tray/lib/system-tray-icon-store.es6 delete mode 100644 internal_packages/system-tray/lib/system-tray.es6 delete mode 100644 internal_packages/system-tray/lib/tray-store.es6 create mode 100644 src/browser/system-tray-manager.es6 diff --git a/internal_packages/system-tray/lib/main.es6 b/internal_packages/system-tray/lib/main.es6 index fdef27273..fb34f42b8 100644 --- a/internal_packages/system-tray/lib/main.es6 +++ b/internal_packages/system-tray/lib/main.es6 @@ -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() { diff --git a/internal_packages/system-tray/lib/system-tray-icon-store.es6 b/internal_packages/system-tray/lib/system-tray-icon-store.es6 new file mode 100644 index 000000000..6b33ea4c3 --- /dev/null +++ b/internal_packages/system-tray/lib/system-tray-icon-store.es6 @@ -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; diff --git a/internal_packages/system-tray/lib/system-tray.es6 b/internal_packages/system-tray/lib/system-tray.es6 deleted file mode 100644 index 903917402..000000000 --- a/internal_packages/system-tray/lib/system-tray.es6 +++ /dev/null @@ -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; diff --git a/internal_packages/system-tray/lib/tray-store.es6 b/internal_packages/system-tray/lib/tray-store.es6 deleted file mode 100644 index 70e60a5fe..000000000 --- a/internal_packages/system-tray/lib/tray-store.es6 +++ /dev/null @@ -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; diff --git a/src/browser/application.coffee b/src/browser/application.coffee index f9c811932..090ea96d8 100644 --- a/src/browser/application.coffee +++ b/src/browser/application.coffee @@ -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) diff --git a/src/browser/system-tray-manager.es6 b/src/browser/system-tray-manager.es6 new file mode 100644 index 000000000..842b12313 --- /dev/null +++ b/src/browser/system-tray-manager.es6 @@ -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; diff --git a/src/error-logger.js b/src/error-logger.js index 3f9df717c..f3a399681 100644 --- a/src/error-logger.js +++ b/src/error-logger.js @@ -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) }