feat(system-tray): add system-tray package

Summary:
- Updates support for ES6 code inside packages
- Displays system tray icon with unread count on darwin, or with bubble on other platforms
- Uses canvas api to dynamically generate icon image given unread count:
  - Adds CavasUtils.canvasFromImgAndText to do this
- Adds config option to display system tray icon on darwin

Test Plan: Need to write the tests for this.

Reviewers: evan, bengotow

Reviewed By: bengotow

Differential Revision: https://phab.nylas.com/D2231
This commit is contained in:
Juan Tejada 2015-11-06 10:47:48 -08:00
parent 29b077dbca
commit d838610290
16 changed files with 234 additions and 3 deletions

View file

@ -26,6 +26,10 @@ class PreferencesGeneral extends React.Component
@props.config.toggle('core.showImportant')
event.preventDefault()
toggleShowSystemTrayIcon: (event) =>
@props.config.toggle('core.showSystemTray')
event.preventDefault()
_renderImportanceOptionElement: =>
return false unless AccountStore.current()?.usesImportantFlag()
importanceOptionElement = <div className="section-header">
@ -45,6 +49,11 @@ class PreferencesGeneral extends React.Component
{@_renderImportanceOptionElement()}
<div className="section-header platform-darwin-only">
<input type="checkbox" id="show-system-tray" checked={@props.config.get('core.showSystemTray')} onChange={@toggleShowSystemTrayIcon}/>
<label htmlFor="show-system-tray">Show Nylas app icon in the menu bar</label>
</div>
<div className="section-header" style={marginTop:30}>
Delay for marking messages as read:
<select value={@props.config.get('core.reading.markAsReadDelay')}

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

View file

@ -0,0 +1,32 @@
import SystemTray from './system-tray';
const platform = process.platform;
let systemTray;
let unsubConfig;
const onSystemTrayToggle = (showSystemTray)=> {
if (showSystemTray.newValue) {
systemTray = new SystemTray(platform);
} else {
systemTray.destroy();
systemTray = null;
}
};
export function activate() {
unsubConfig = atom.config.onDidChange('core.showSystemTray', onSystemTrayToggle);
if (atom.config.get('core.showSystemTray')) {
systemTray = new SystemTray(platform);
}
}
export function deactivate() {
if (systemTray) {
systemTray.destroy();
systemTray = null;
}
unsubConfig();
}
export function serialize() {
}

View file

@ -0,0 +1,49 @@
import remote from 'remote';
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());
const menu = this._store.menu();
if (menu != null) this._tray.setContextMenu(menu);
this._unsubscribe = this._addEventListeners();
}
_addEventListeners() {
this._tray.addListener('clicked', this._onClicked.bind(this));
const unsubClicked = ()=> this._tray.removeListener('clicked', this._onClicked);
const unsubStore = this._store.listen(this._onChange.bind(this));
return ()=> {
unsubClicked();
unsubStore();
};
}
_onClicked() {
if (this._platform !== 'darwin') {
atom.focus();
}
}
_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

@ -0,0 +1,98 @@
import path from 'path';
import remote from 'remote';
import NylasStore from 'nylas-store';
import {UnreadCountStore, CanvasUtils} from 'nylas-exports';
const WindowManager = remote.getGlobal('application').windowManager;
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: ()=> WindowManager.sendToMainWindow('new-message'),
},
{
label: 'Preferences',
click: ()=> WindowManager.sendToMainWindow('open-preferences'),
},
{
type: 'separator',
},
{
label: 'Quit N1',
click: ()=> atom.quit(),
},
];
const _buildMenu = (platform)=> {
if (platform === 'darwin') {
menuTemplate.unshift({
label: 'Open inbox',
click: ()=> atom.focus(),
});
}
return Menu.buildFromTemplate(menuTemplate);
};
class TrayStore extends NylasStore {
constructor(platform) {
super();
this._platform = platform;
this._unreadIcon = NativeImage.createFromPath(UNREAD_ICON_PATH);
this._baseIcon = NativeImage.createFromPath(BASE_ICON_PATH);
this._unreadCount = UnreadCountStore.count() || 0;
this._menu = _buildMenu(platform);
this._icon = this._getIconImg();
this.listenTo(UnreadCountStore, this._onUnreadCountChanged);
}
unreadCount() {
return this._unreadCount;
}
icon() {
return this._icon;
}
tooltip() {
return `${this._unreadCount} unread messages`;
}
menu() {
return this._menu;
}
_getIconImg(platform = this._platform, unreadCount = this._unreadCount) {
const imgHandlers = {
'darwin': ()=> {
const img = new Image();
// This is synchronous because it's a data url
img.src = this._baseIcon.toDataUrl();
const count = this._unreadCount || '';
const canvas = canvasWithSystemTrayIconAndText(img, count.toString());
return NativeImage.createFromDataUrl(canvas.toDataURL());
},
'default': ()=> {
return unreadCount > 0 ? this._unreadIcon : this._baseIcon;
},
};
return imgHandlers[platform in imgHandlers ? platform : 'default']();
}
_onUnreadCountChanged() {
this._unreadCount = UnreadCountStore.count();
this._icon = this._getIconImg();
this.trigger();
}
}
export default TrayStore;

View file

@ -0,0 +1,12 @@
{
"name": "system-tray",
"version": "0.1.0",
"main": "./lib/main",
"description": "Displays cross-platform system tray icon with unread count",
"license": "Proprietary",
"private": true,
"engines": {
"atom": "*"
},
"dependencies": {}
}

View file

@ -13,10 +13,13 @@
},
"electronVersion": "0.29.2",
"dependencies": {
"6to5-core": "^3.5",
"asar": "^0.5.0",
"async": "^0.9",
"atom-keymap": "^5.1",
"babel-core": "^6.0.20",
"babel-preset-es2015": "^6.0.15",
"babel-preset-react": "^6.0.15",
"babel-preset-stage-0": "^6.0.15",
"bluebird": "^2.9",
"classnames": "1.2.1",
"clear-cut": "0.4.0",

View file

@ -5,6 +5,8 @@ DragCanvas = document.createElement("canvas")
DragCanvas.style.position = "absolute"
document.body.appendChild(DragCanvas)
SystemTrayCanvas = document.createElement("canvas")
CanvasUtils =
roundRect: (ctx, x, y, width, height, radius = 5, fill, stroke = true) ->
ctx.beginPath()
@ -70,4 +72,23 @@ CanvasUtils =
return DragCanvas
canvasWithSystemTrayIconAndText: (img, text) ->
canvas = SystemTrayCanvas
w = img.width
h = img.height
# Rough estimate of extra width to hold text
canvas.width = w + (10 * text.length)
canvas.height = h
context = canvas.getContext('2d')
context.font = '14px Nylas-Pro'
context.fillStyle = 'black'
context.textAlign = 'start'
context.textBaseline = 'middle'
context.drawImage(img, 0, 0)
# Place after img, vertically aligned
context.fillText(text, w + 2, h / 2)
return canvas
module.exports = CanvasUtils

View file

@ -17,6 +17,9 @@ module.exports =
showImportant:
type: 'boolean'
default: true
showSystemTray:
type: 'boolean'
default: true
disabledPackages:
type: 'array'
default: []

View file

@ -15,8 +15,12 @@ function registerRuntimeTranspilers(hotreload) {
} else {
require('coffee-react/register');
}
// This redefines require.extensions['.js'].
require('../src/6to5').register();
// This redefines require.extensions
require('babel-core/register')({
sourceMaps: true,
presets: ['es2015', 'react', 'stage-0'],
extensions: ['.es6', '.es', '.jsx'],
});
}
window.onload = function() {