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
|
@ -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')}
|
||||
|
|
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 19 KiB |
After Width: | Height: | Size: 18 KiB |
After Width: | Height: | Size: 20 KiB |
After Width: | Height: | Size: 19 KiB |
32
internal_packages/system-tray/lib/main.es6
Normal 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() {
|
||||
|
||||
}
|
49
internal_packages/system-tray/lib/system-tray.es6
Normal 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;
|
98
internal_packages/system-tray/lib/tray-store.es6
Normal 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;
|
12
internal_packages/system-tray/package.json
Normal 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": {}
|
||||
}
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -17,6 +17,9 @@ module.exports =
|
|||
showImportant:
|
||||
type: 'boolean'
|
||||
default: true
|
||||
showSystemTray:
|
||||
type: 'boolean'
|
||||
default: true
|
||||
disabledPackages:
|
||||
type: 'array'
|
||||
default: []
|
||||
|
|
|
@ -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() {
|
||||
|
|