feat(keymap): add new <KeymapHandlers />

Summary:
Refactor keymaps to wrap components with a <KeymapHandlers /> component.
This more Reactful way of declaring keyback handlers prevents us from
needing to subscribe to `atom.commands`

Test Plan: new tests

Reviewers: bengotow, juan

Reviewed By: bengotow

Differential Revision: https://phab.nylas.com/D2226
This commit is contained in:
Evan Morikawa 2015-11-06 11:47:06 -08:00
parent 118761d79e
commit 455b418d6f
35 changed files with 935 additions and 681 deletions

View file

@ -1,24 +1,26 @@
# Your keymap
#
# Atom keymaps work similarly to stylesheets. Just as stylesheets use selectors
# to apply styles to elements, Atom keymaps use selectors to associate
# keystrokes with events in specific contexts.
# N1 keymaps work in conjunction with the {KeyCommandsRegion} React
# component.
#
# You can create a new keybinding in this file by typing "key" and then hitting
# tab.
# A key, or sequence of keys is first mapped to a "command".
#
# Here's an example taken from Atom's built-in keymap:
# The "command" is then mapped to a callback function within your React
# component or store.
#
# 'atom-text-editor':
# 'enter': 'editor:newline'
# The keyboard -> command mapping is defined in this `.cson` file. Each
# mapping is scoped under the component that it applies to by matching the
# root-level CSS class of that component.
#
# '.workspace':
# 'ctrl-shift-p': 'core:move-up'
# 'ctrl-p': 'core:move-down'
# Any global, top-level mappings are scoped under the `body` selector.
#
# You can find more information about keymaps in these guides:
# * https://atom.io/docs/latest/customizing-atom#customizing-key-bindings
# * https://atom.io/docs/latest/advanced/keymaps
# For example:
#
# 'body':
# 'ctrl-c': 'application:new-message'
#
# '.my-custom-package':
# 'ctrl-p': 'myPackage:customAction'
#
# This file uses CoffeeScript Object Notation (CSON).
# If you are unfamiliar with CSON, you can read more about it here:

View file

@ -1,7 +1,7 @@
shell = require 'shell'
GithubStore = require './github-store'
{React} = require 'nylas-exports'
{RetinaImg} = require 'nylas-component-kit'
{RetinaImg, KeyCommandsRegion} = require 'nylas-component-kit'
###
The `ViewOnGithubButton` displays a button whenever there's a relevant
@ -68,23 +68,24 @@ class ViewOnGithubButton extends React.Component
# are cleaned up. Every time the `GithubStore` calls its `trigger`
# method, the `_onStoreChanged` callback will be fired.
@_unlisten = GithubStore.listen(@_onStoreChanged)
@_keymapUnlisten = atom.commands.add 'body', {
'github:open': @_openLink
}
componentWillUnmount: ->
@_unlisten?()
@_keymapUnlisten?.dispose()
_keymapHandlers: ->
'github:open': @_openLink
render: ->
return null unless @state.link
<button className="btn btn-toolbar"
onClick={@_openLink}
title={"Visit Thread on GitHub"}>
<RetinaImg
mode={RetinaImg.Mode.ContentIsMask}
url="nylas://N1-Message-View-on-Github/assets/github@2x.png" />
</button>
<KeyCommandsRegion globalHandlers={@_keymapHandlers()}>
<button className="btn btn-toolbar"
onClick={@_openLink}
title={"Visit Thread on GitHub"}>
<RetinaImg
mode={RetinaImg.Mode.ContentIsMask}
url="nylas://N1-Message-View-on-Github/assets/github@2x.png" />
</button>
</KeyCommandsRegion>
#### Super common N1 Component private methods ####

View file

@ -19,6 +19,7 @@ React = require 'react'
{Menu,
Popover,
RetinaImg,
KeyCommandsRegion,
LabelColorizer} = require 'nylas-component-kit'
# This changes the category on one or more threads.
@ -37,9 +38,6 @@ class CategoryPicker extends React.Component
@unsubscribers.push CategoryStore.listen @_onStoreChanged
@unsubscribers.push AccountStore.listen @_onStoreChanged
@_commandUnsubscriber = atom.commands.add 'body',
"application:change-category": @_onOpenCategoryPopover
# If the threads we're picking categories for change, (like when they
# get their categories updated), we expect our parents to pass us new
# props. We don't listen to the DatabaseStore ourselves.
@ -50,7 +48,9 @@ class CategoryPicker extends React.Component
componentWillUnmount: =>
return unless @unsubscribers
unsubscribe() for unsubscribe in @unsubscribers
@_commandUnsubscriber.dispose()
_keymapHandlers: ->
"application:change-category": @_onOpenCategoryPopover
render: =>
return <span></span> unless @_account
@ -84,23 +84,25 @@ class CategoryPicker extends React.Component
onChange={@_onSearchValueChange}/>
]
<Popover className="category-picker"
ref="popover"
onOpened={@_onPopoverOpened}
onClosed={@_onPopoverClosed}
direction="down-align-left"
style={order: -103}
buttonComponent={button}>
<Menu ref="menu"
headerComponents={headerComponents}
footerComponents={[]}
items={@state.categoryData}
itemKey={ (item) -> item.id }
itemContent={@_renderItemContent}
onSelect={@_onSelectCategory}
defaultSelectedIndex={if @state.searchValue is "" then -1 else 0}
/>
</Popover>
<KeyCommandsRegion globalHandlers={@_keymapHandlers()}>
<Popover className="category-picker"
ref="popover"
onOpened={@_onPopoverOpened}
onClosed={@_onPopoverClosed}
direction="down-align-left"
style={order: -103}
buttonComponent={button}>
<Menu ref="menu"
headerComponents={headerComponents}
footerComponents={[]}
items={@state.categoryData}
itemKey={ (item) -> item.id }
itemContent={@_renderItemContent}
onSelect={@_onSelectCategory}
defaultSelectedIndex={if @state.searchValue is "" then -1 else 0}
/>
</Popover>
</KeyCommandsRegion>
_onOpenCategoryPopover: =>
return unless @_threads().length > 0

View file

@ -1,31 +1,16 @@
'.composer-outer-wrap, .composer-outer-wrap input, .composer-outer-wrap
div[contenteditable]':
'cmd-B' : 'composer:show-and-focus-bcc'
'cmd-C' : 'composer:show-and-focus-cc'
'ctrl-B' : 'composer:show-and-focus-bcc'
'ctrl-C' : 'composer:show-and-focus-cc'
'cmd-T' : 'composer:focus-to'
'ctrl-T' : 'composer:focus-to'
'cmd-enter' : 'composer:send-message'
'ctrl-enter' : 'composer:send-message'
'cmdctrl-T' : 'composer:focus-to'
'cmdctrl-C' : 'composer:show-and-focus-cc'
'cmdctrl-B' : 'composer:show-and-focus-bcc'
'cmdctrl-F' : 'composer:show-and-focus-from'
'cmdctrl-enter': 'composer:send-message'
'cmdctrl-z' : 'composer:undo'
'cmdctrl-Z' : 'composer:redo'
'cmdctrl-y' : 'composer:redo'
'.composer-outer-wrap':
'delete' : 'composer:no-op'
'delete': 'composer:no-op'
'.composer-outer-wrap, .composer-outer-wrap div[contenteditable]':
'escape' : 'composer:delete-empty-draft'
'body.platform-win32 .composer-outer-wrap *[contenteditable], body.platform-win32 .composer-outer-wrap input':
'ctrl-z': 'composer:undo'
'ctrl-Z': 'composer:redo'
'ctrl-y': 'composer:redo'
'body.platform-linux .composer-outer-wrap *[contenteditable], body.platform-linux .composer-outer-wrap input':
'ctrl-z': 'composer:undo'
'ctrl-Z': 'composer:redo'
'ctrl-y': 'composer:redo'
'body.platform-darwin .composer-outer-wrap *[contenteditable], body.platform-darwin .composer-outer-wrap input':
'cmd-z': 'composer:undo'
'cmd-Z': 'composer:redo'
'cmd-y': 'composer:redo'
'escape': 'composer:delete-empty-draft'

View file

@ -20,6 +20,7 @@ class CollapsedParticipants extends React.Component
bcc: []
constructor: (@props={}) ->
@_keyPrefix = Utils.generateTempId()
@state =
numToDisplay: 999
numRemaining: 0
@ -60,15 +61,17 @@ class CollapsedParticipants extends React.Component
return <div className="num-remaining-wrap tokenizing-field"><div className="show-more-fade"></div><div className="num-remaining token">{str}</div></div>
_collapsedContact: (contact) ->
_collapsedContact: (contact) =>
name = contact.displayName()
<span key={contact.id}
key = @_keyPrefix + contact.email + contact.name
<span key={key}
className="collapsed-contact regular-contact">{name}</span>
_collapsedBccContact: (contact, i) ->
_collapsedBccContact: (contact, i) =>
name = contact.displayName()
key = @_keyPrefix + contact.email + contact.name
if i is 0 then name = "Bcc: #{name}"
<span key={contact.id}
<span key={key}
className="collapsed-contact bcc-contact">{name}</span>
_setNumHiddenParticipants: ->

View file

@ -17,6 +17,7 @@ React = require 'react'
ScrollRegion,
Contenteditable,
InjectedComponent,
KeyCommandsRegion,
FocusTrackingRegion,
InjectedComponentSet} = require 'nylas-component-kit'
@ -77,15 +78,6 @@ class ComposerView extends React.Component
@_usubs = []
@_usubs.push FileUploadStore.listen @_onFileUploadStoreChange
@_usubs.push AccountStore.listen @_onAccountStoreChanged
@_keymapUnlisten = atom.commands.add '.composer-outer-wrap', {
'composer:send-message': => @_sendDraft()
'composer:delete-empty-draft': => @_deleteDraftIfEmpty()
'composer:show-and-focus-bcc': => @_onChangeEnabledFields(show: [Fields.Bcc], focus: Fields.Bcc)
'composer:show-and-focus-cc': => @_onChangeEnabledFields(show: [Fields.Cc], focus: Fields.Cc)
'composer:focus-to': => @_onChangeEnabledFields(show: [Fields.To], focus: Fields.To)
"composer:undo": @undo
"composer:redo": @redo
}
@_applyFocusedField()
componentWillUnmount: =>
@ -93,7 +85,6 @@ class ComposerView extends React.Component
@_teardownForDraft()
@_deleteDraftIfEmpty()
usub() for usub in @_usubs
@_keymapUnlisten.dispose() if @_keymapUnlisten
componentDidUpdate: =>
# We want to use a temporary variable instead of putting this into the
@ -105,6 +96,19 @@ class ComposerView extends React.Component
@_applyFocusedField()
_keymapHandlers: ->
'composer:send-message': => @_sendDraft()
'composer:delete-empty-draft': => @_deleteDraftIfEmpty()
'composer:show-and-focus-bcc': =>
@_onChangeEnabledFields(show: [Fields.Bcc], focus: Fields.Bcc)
'composer:show-and-focus-cc': =>
@_onChangeEnabledFields(show: [Fields.Cc], focus: Fields.Cc)
'composer:focus-to': =>
@_onChangeEnabledFields(show: [Fields.To], focus: Fields.To)
"composer:show-and-focus-from": => # TODO
"composer:undo": @undo
"composer:redo": @redo
_applyFocusedField: ->
if @state.focusedField
return unless @refs[@state.focusedField]
@ -130,7 +134,7 @@ class ComposerView extends React.Component
@undoManager = new UndoManager
DraftStore.sessionForClientId(draftClientId).then(@_setupSession)
_setupSession: (proxy) =>
__setupSessionsetupSession: (proxy) =>
return if @_unmounted
return unless proxy.draftClientId is @props.draftClientId
@_proxy = proxy
@ -149,20 +153,26 @@ class ComposerView extends React.Component
if @_proxy
@_proxy.changes.commit()
render: =>
render: ->
<KeyCommandsRegion localHandlers={@_keymapHandlers()}
className="composer-outer-wrap">
{@_renderComposerWrap()}
</KeyCommandsRegion>
_renderComposerWrap: =>
if @props.mode is "inline"
<FocusTrackingRegion className={@_wrapClasses()}
ref="composer"
ref="composerWrap"
tabIndex="-1">
{@_renderComposer()}
</FocusTrackingRegion>
else
<div className={@_wrapClasses()} ref="composer">
<div className={@_wrapClasses()} ref="composerWrap">
{@_renderComposer()}
</div>
_wrapClasses: =>
"message-item-white-wrap composer-outer-wrap #{@props.className ? ""}"
"message-item-white-wrap #{@props.className ? ""}"
_renderComposer: =>
<DropZone className="composer-inner-wrap"
@ -320,7 +330,7 @@ class ComposerView extends React.Component
# component. We provide it our boundingClientRect so it can calculate
# this value.
_getComposerBoundingRect: =>
React.findDOMNode(@refs.composer).getBoundingClientRect()
React.findDOMNode(@refs.composerWrap).getBoundingClientRect()
_onScrollToBottom: ->
if @props.onRequestScrollTo

View file

@ -133,6 +133,9 @@ describe "populated composer", ->
@isSending = {state: false}
spyOn(DraftStore, "isSendingDraft").andCallFake => @isSending.state
afterEach ->
DraftStore._cleanupAllSessions()
describe "when sending a new message", ->
it 'makes a request with the message contents', ->
useDraft.call @
@ -528,20 +531,21 @@ describe "populated composer", ->
useFullDraft.apply(@)
makeComposer.call(@)
NylasTestUtils.loadKeymap("internal_packages/composer/keymaps/composer")
@$composer = @composer.refs.composerWrap
it "sends the draft on cmd-enter", ->
NylasTestUtils.keyPress("cmd-enter", React.findDOMNode(@composer))
NylasTestUtils.keyPress("cmd-enter", React.findDOMNode(@$composer))
expect(Actions.sendDraft).toHaveBeenCalled()
it "does not send the draft on enter if the button isn't in focus", ->
NylasTestUtils.keyPress("enter", React.findDOMNode(@composer))
NylasTestUtils.keyPress("enter", React.findDOMNode(@$composer))
expect(Actions.sendDraft).not.toHaveBeenCalled()
it "doesn't let you send twice", ->
NylasTestUtils.keyPress("cmd-enter", React.findDOMNode(@composer))
NylasTestUtils.keyPress("cmd-enter", React.findDOMNode(@$composer))
@isSending.state = true
DraftStore.trigger()
NylasTestUtils.keyPress("cmd-enter", React.findDOMNode(@composer))
NylasTestUtils.keyPress("cmd-enter", React.findDOMNode(@$composer))
expect(Actions.sendDraft).toHaveBeenCalled()
expect(Actions.sendDraft.calls.length).toBe 1

View file

@ -10,18 +10,19 @@ MessageItemContainer = require './message-item-container'
MessageStore,
DatabaseStore,
WorkspaceStore,
ComponentRegistry,
ChangeLabelsTask,
ComponentRegistry,
ChangeStarredTask} = require("nylas-exports")
{Spinner,
RetinaImg,
MailLabel,
ScrollRegion,
ResizableRegion,
RetinaImg,
InjectedComponentSet,
MailLabel,
MailImportantIcon,
InjectedComponent} = require('nylas-component-kit')
InjectedComponent,
KeyCommandsRegion,
InjectedComponentSet} = require('nylas-component-kit')
class MessageListScrollTooltip extends React.Component
@displayName: 'MessageListScrollTooltip'
@ -73,18 +74,8 @@ class MessageList extends React.Component
@_unsubscribers = []
@_unsubscribers.push MessageStore.listen @_onChange
commands = _.extend {},
'application:reply': => @_createReplyOrUpdateExistingDraft('reply')
'application:reply-all': => @_createReplyOrUpdateExistingDraft('reply-all')
'application:forward': => @_onForward()
'core:messages-page-up': => @_onScrollByPage(-1)
'core:messages-page-down': => @_onScrollByPage(1)
@command_unsubscriber = atom.commands.add('body', commands)
componentWillUnmount: =>
unsubscribe() for unsubscribe in @_unsubscribers
@command_unsubscriber.dispose()
shouldComponentUpdate: (nextProps, nextState) =>
not Utils.isEqualReact(nextProps, @props) or
@ -97,6 +88,13 @@ class MessageList extends React.Component
if newDraftClientIds.length > 0
@_focusDraft(@_getMessageContainer(newDraftClientIds[0]))
_keymapHandlers: ->
'application:reply': => @_createReplyOrUpdateExistingDraft('reply')
'application:reply-all': => @_createReplyOrUpdateExistingDraft('reply-all')
'application:forward': => @_onForward()
'core:messages-page-up': => @_onScrollByPage(-1)
'core:messages-page-down': => @_onScrollByPage(1)
_newDraftClientIds: (prevState) =>
oldDraftIds = _.map(_.filter((prevState.messages ? []), (m) -> m.draft), (m) -> m.clientId)
newDraftIds = _.map(_.filter((@state.messages ? []), (m) -> m.draft), (m) -> m.clientId)
@ -191,26 +189,28 @@ class MessageList extends React.Component
"messages-wrap": true
"ready": not @state.loading
<div className="message-list" id="message-list">
<ScrollRegion tabIndex="-1"
className={wrapClass}
scrollTooltipComponent={MessageListScrollTooltip}
ref="messageWrap">
{@_renderSubject()}
<div className="headers" style={position:'relative'}>
<InjectedComponentSet
className="message-list-notification-bars"
matching={role:"MessageListNotificationBar"}
exposedProps={thread: @state.currentThread}/>
<InjectedComponentSet
className="message-list-headers"
matching={role:"MessageListHeaders"}
exposedProps={thread: @state.currentThread}/>
</div>
{@_messageElements()}
</ScrollRegion>
<Spinner visible={@state.loading} />
</div>
<KeyCommandsRegion globalHandlers={@_keymapHandlers()}>
<div className="message-list" id="message-list">
<ScrollRegion tabIndex="-1"
className={wrapClass}
scrollTooltipComponent={MessageListScrollTooltip}
ref="messageWrap">
{@_renderSubject()}
<div className="headers" style={position:'relative'}>
<InjectedComponentSet
className="message-list-notification-bars"
matching={role:"MessageListNotificationBar"}
exposedProps={thread: @state.currentThread}/>
<InjectedComponentSet
className="message-list-headers"
matching={role:"MessageListHeaders"}
exposedProps={thread: @state.currentThread}/>
</div>
{@_messageElements()}
</ScrollRegion>
<Spinner visible={@state.loading} />
</div>
</KeyCommandsRegion>
_renderSubject: ->
subject = @state.currentThread?.subject
@ -300,7 +300,7 @@ class MessageList extends React.Component
<div className="num-messages">{bundle.messages.length} older messages</div>
<div className="msg-lines" style={height: h*lines.length}>
{lines.map (msg, i) ->
<div style={height: h*2, top: -h*i} className="msg-line"></div>}
<div key={msg.id} style={height: h*2, top: -h*i} className="msg-line"></div>}
</div>
</div>

View file

@ -12,7 +12,7 @@ DisplayedKeybindings = [
['application:focus-search', 'Search'],
['application:change-category', 'Change Folder / Labels'],
['core:select-item', 'Select Focused Item'],
['core:star-item', 'Star Focused Item'],
['application:star-item', 'Star Focused Item'],
]
class PreferencesKeymaps extends React.Component

View file

@ -1,7 +1,7 @@
React = require 'react/addons'
classNames = require 'classnames'
{Actions, WorkspaceStore} = require 'nylas-exports'
{Menu, RetinaImg} = require 'nylas-component-kit'
{Menu, RetinaImg, KeyCommandsRegion} = require 'nylas-component-kit'
SearchSuggestionStore = require './search-suggestion-store'
_ = require 'underscore'
@ -20,20 +20,16 @@ class SearchBar extends React.Component
@usub.push SearchSuggestionStore.listen @_onChange
@usub.push WorkspaceStore.listen =>
@setState(focused: false) if @state.focused
@body_unsubscriber = atom.commands.add 'body', {
'application:focus-search': @_onFocusSearch
}
@search_unsubscriber = atom.commands.add '.search-bar', {
'search-bar:escape-search': @_clearAndBlur
}
# It's important that every React class explicitly stops listening to
# atom events before it unmounts. Thank you event-kit
# This can be fixed via a Reflux mixin
componentWillUnmount: =>
usub() for usub in @usub
@body_unsubscriber.dispose()
@search_unsubscriber.dispose()
_keymapHandlers: ->
'application:focus-search': @_onFocusSearch
'search-bar:escape-search': @_clearAndBlur
render: =>
inputValue = @_queryToString(@state.query)
@ -74,16 +70,18 @@ class SearchBar extends React.Component
else
item.label
<div className="search-bar">
<Menu ref="menu"
className={@_containerClasses()}
headerComponents={headerComponents}
items={@state.suggestions}
itemContent={itemContentFunc}
itemKey={ (item) -> item.id ? item.label }
onSelect={@_onSelectSuggestion}
/>
</div>
<KeyCommandsRegion className="search-bar" globalHandlers={@_keymapHandlers()}>
<div>
<Menu ref="menu"
className={@_containerClasses()}
headerComponents={headerComponents}
items={@state.suggestions}
itemContent={itemContentFunc}
itemKey={ (item) -> item.id ? item.label }
onSelect={@_onSelectSuggestion}
/>
</div>
</KeyCommandsRegion>
_onFocusSearch: =>
React.findDOMNode(@refs.searchInput).focus()

View file

@ -5,6 +5,7 @@ classNames = require 'classnames'
MultiselectList,
RetinaImg,
MailLabel,
KeyCommandsRegion,
InjectedComponentSet} = require 'nylas-component-kit'
{timestamp, subject} = require './formatting-utils'
{Actions,
@ -166,24 +167,6 @@ class ThreadList extends React.Component
@narrowColumns = [cNarrow]
_shift = ({offset, afterRunning}) =>
view = ThreadListStore.view()
focusedId = FocusedContentStore.focusedId('thread')
focusedIdx = Math.min(view.count() - 1, Math.max(0, view.indexOfId(focusedId) + offset))
item = view.get(focusedIdx)
afterRunning()
Actions.setFocus(collection: 'thread', item: item)
@commands =
'core:remove-from-view': @_onRemoveFromView
'core:archive-item': @_onArchiveItem
'core:delete-item': @_onDeleteItem
'core:star-item': @_onStarItem
'core:remove-and-previous': =>
_shift(offset: 1, afterRunning: @_onRemoveFromView)
'core:remove-and-next': =>
_shift(offset: -1, afterRunning: @_onRemoveFromView)
@itemPropsProvider = (item) ->
className: classNames
'unread': item.unread
@ -196,12 +179,35 @@ class ThreadList extends React.Component
componentWillUnmount: =>
window.removeEventListener('resize', @_onResize, true)
render: =>
_shift: ({offset, afterRunning}) =>
view = ThreadListStore.view()
focusedId = FocusedContentStore.focusedId('thread')
focusedIdx = Math.min(view.count() - 1, Math.max(0, view.indexOfId(focusedId) + offset))
item = view.get(focusedIdx)
afterRunning()
Actions.setFocus(collection: 'thread', item: item)
_keymapHandlers: ->
'core:remove-from-view': @_onRemoveFromView
'application:archive-item': @_onArchiveItem
'application:delete-item': @_onDeleteItem
'application:star-item': @_onStarItem
'application:remove-and-previous': =>
@_shift(offset: 1, afterRunning: @_onRemoveFromView)
'application:remove-and-next': =>
@_shift(offset: -1, afterRunning: @_onRemoveFromView)
render: ->
<KeyCommandsRegion globalHandlers={@_keymapHandlers()}
className="thread-list-wrap">
{@_renderList()}
</KeyCommandsRegion>
_renderList: =>
if @state.style is 'wide'
<MultiselectList
dataStore={ThreadListStore}
columns={@wideColumns}
commands={@commands}
itemPropsProvider={@itemPropsProvider}
itemHeight={39}
className="thread-list"
@ -215,7 +221,6 @@ class ThreadList extends React.Component
<MultiselectList
dataStore={ThreadListStore}
columns={@narrowColumns}
commands={@commands}
itemPropsProvider={@itemPropsProvider}
itemHeight={90}
className="thread-list thread-list-narrow"
@ -260,8 +265,6 @@ class ThreadList extends React.Component
if current isnt desired
@setState(style: desired)
# Additional Commands
_threadsForKeyboardAction: ->
return null unless ThreadListStore.view()
focused = FocusedContentStore.focused('thread')

View file

@ -1,39 +1,13 @@
# Note: For a menu item to have a keyboard equiavalent, it needs
# to be listed in this file.
# Note: Only put keymaps in here that apply to Mac ONLY
#
# Most cross-platform issues can be resolved by using the special N1
# `cmdctrl` extension before you keymap. This will automatically default
# to using the `cmd` key on Mac and `ctrl` key on Windows.
'body':
# Mac application keys
'cmd-q': 'application:quit'
'cmd-m': 'application:minimize'
'cmd-h': 'application:hide'
'cmd-1': 'application:show-main-window'
'cmd-m': 'application:minimize'
'cmd-alt-w': 'application:show-work-window'
'cmd-alt-h': 'application:hide-other-applications'
'alt-cmd-ctrl-m': 'application:zoom'
'cmd-alt-ctrl-s': 'application:run-all-specs'
# Mac core keys
'cmd-z': 'core:undo'
'cmd-Z': 'core:redo'
'cmd-y': 'core:redo'
'cmd-x': 'core:cut'
'cmd-c': 'core:copy'
'cmd-a': 'core:select-all'
'cmd-v': 'core:paste'
# Mac window keys
'cmd-w': 'window:close'
'cmd-=': 'window:increase-font-size'
'cmd-+': 'window:increase-font-size'
'cmd--': 'window:decrease-font-size'
'cmd-_': 'window:decrease-font-size'
'cmd-0': 'window:reset-font-size'
'alt-cmd-i': 'window:toggle-dev-tools'
'cmd-ctrl-f': 'window:toggle-full-screen'
'ctrl-alt-cmd-l': 'window:reload'
'cmd-alt-ctrl-p': 'application:run-package-specs'
'body *[contenteditable]':
'cmd-z': 'native!'
'cmd-Z': 'native!'
'cmd-y': 'native!'
'alt-cmd-ctrl-m': 'application:zoom'

View file

@ -1,36 +1,11 @@
# Note: For a menu item to have a keyboard equiavalent, it needs
# to be listed in this file.
# Note: Only put keymaps in here that apply to Linux ONLY
#
# Most cross-platform issues can be resolved by using the special N1
# `cmdctrl` extension before you keymap. This will automatically default
# to using the `cmd` key on Mac and `ctrl` key on Windows.
# Linux email-specific menu items
'body':
# Linux application keys
'ctrl-q': 'application:quit'
# Linux core keys
'ctrl-z': 'core:undo'
'ctrl-Z': 'core:redo'
'ctrl-y': 'core:redo'
'ctrl-x': 'core:cut'
'ctrl-c': 'core:copy'
'ctrl-a': 'core:select-all'
'ctrl-v': 'core:paste'
'ctrl-insert': 'core:copy'
'shift-insert': 'core:paste'
# Linux window keys
'ctrl-w': 'core:close'
'ctrl-=': 'window:increase-font-size'
'ctrl-+': 'window:increase-font-size'
'ctrl--': 'window:decrease-font-size'
'ctrl-_': 'window:decrease-font-size'
'ctrl-0': 'window:reset-font-size'
'ctrl-alt-r': 'window:reload'
'ctrl-shift-i': 'window:toggle-dev-tools'
'ctrl-alt-p': 'application:run-package-specs'
'ctrl-alt-s': 'application:run-all-specs'
'F11': 'window:toggle-full-screen'
'body *[contenteditable]':
'ctrl-z': 'native!'
'ctrl-Z': 'native!'
'ctrl-y': 'native!'

View file

@ -1,43 +1,11 @@
# Note: For a menu item to have a keyboard equiavalent, it needs
# to be listed in this file.
# Note: Only put keymaps in here that apply to Windows ONLY
#
# Most cross-platform issues can be resolved by using the special N1
# `cmdctrl` extension before you keymap. This will automatically default
# to using the `cmd` key on Mac and `ctrl` key on Windows.
'body':
# Windows email-specific menu items
'ctrl-n': 'application:new-message' # Outlook
'ctrl-r': 'application:reply' # Outlook
'ctrl-R': 'application:reply-all' # Outlook
'ctrl-F': 'application:forward' # Outlook
'ctrl-shift-v': 'application:change-category' # Outlook
# Windows application keys
'ctrl-q': 'application:quit'
'ctrl-alt-s': 'application:run-all-specs'
# Windows core keys
'ctrl-z': 'core:undo'
'ctrl-Z': 'core:redo'
'ctrl-y': 'core:redo'
'ctrl-x': 'core:cut'
'ctrl-c': 'core:copy'
'ctrl-a': 'core:select-all'
'ctrl-v': 'core:paste'
'ctrl-insert': 'core:copy'
'shift-insert': 'core:paste'
# Windows window keys
'ctrl-w': 'window:close'
'ctrl-=': 'window:increase-font-size'
'ctrl-+': 'window:increase-font-size'
'ctrl--': 'window:decrease-font-size'
'ctrl-_': 'window:decrease-font-size'
'ctrl-0': 'window:reset-font-size'
'ctrl-alt-r': 'window:reload'
'ctrl-alt-i': 'window:toggle-dev-tools'
'ctrl-alt-p': 'application:run-package-specs'
'ctrl-alt-s': 'application:run-all-specs'
'cmd-alt-l': 'window:reload'
'cmd-alt-i': 'window:toggle-dev-tools'
'cmd-alt-w': 'application:show-work-window'
'F11': 'window:toggle-full-screen'
'body *[contenteditable]':
'ctrl-z': 'native!'
'ctrl-Z': 'native!'
'ctrl-y': 'native!'

View file

@ -1,202 +1,97 @@
# Email-specific core key-mappings
# This is the core set of universal, cross-platform keymaps. This is
# extended in the following places:
#
# There are additional mappings in <platform>.cson files that bind
# menu items. In the future, we should break these into files like:
# darwin-gmail.cson, darwin-macmail.cson, win32-gmail.cson...
# 1. keymaps/base.cson - (This file) Core, universal keymaps across all platforms
# 2. keymaps/base-darwin.cson - Any universal mac-only keymaps
# 3. keymaps/base-win32.cson - Any universal windows-only keymaps
# 4. keymaps/base-darwin.cson - Any universal linux-only keymaps
# 5. keymaps/templates/Gmail.cson - Gmail key bindings for all platforms
# 6. keymaps/templates/Outlook.cson - Outlook key bindings for all platforms
# 7. keymaps/templates/Apple Mail.cson - Mac Mail key bindings for all platforms
# 8. some/package/keymaps/package.cson - Keymaps for a specific package
# 9. ~/.nylas/keymap.cson - Custom user-specific overrides
#
# NOTE: We have a special N1 extension called `cmdctrl` that automatically
# uses `cmd` on mac and `ctrl` on windows and linux. This covers most
# cross-platform cases. For truely platform-specific features, use the
# platform keymap extensions.
'body':
'escape' : 'application:pop-sheet'
'cmd-,' : 'application:open-preferences'
'up' : 'core:previous-item'
'down' : 'core:next-item'
'enter' : 'core:focus-item'
'delete' : 'core:remove-from-view'
'backspace': 'core:remove-from-view'
'pageup' : 'core:messages-page-up'
'pagedown' : 'core:messages-page-down'
'shift-pageup' : 'core:list-page-up'
'shift-pagedown' : 'core:list-page-down'
# Default cross-platform core behaviors
'left': 'core:move-left'
'right': 'core:move-right'
'shift-up': 'core:select-up'
'shift-down': 'core:select-down'
'shift-left': 'core:select-left'
'shift-right': 'core:select-right'
### Core system commands. ###
# These have their default effects, but map to
# commands to allow for custom interactions.
'cmdctrl-z': 'core:undo'
'cmdctrl-Z': 'core:redo'
'cmdctrl-y': 'core:redo'
'cmdctrl-x': 'core:cut'
'cmdctrl-c': 'core:copy'
'cmdctrl-v': 'core:paste'
'cmdctrl-a': 'core:select-all'
'shift-delete': 'core:cut'
# Inputs are native by default.
# Also make sure not to catch anything intended for a webview
'body input, body textarea, body *[contenteditable], body webview':
'up': 'native!'
'left': 'native!'
'down': 'native!'
'right': 'native!'
'cmd-up': 'native!'
'cmd-left': 'native!'
'cmd-down': 'native!'
'cmd-right': 'native!'
'ctrl-up': 'native!'
'ctrl-left': 'native!'
'ctrl-down': 'native!'
'ctrl-right': 'native!'
'shift-up': 'native!'
'shift-left': 'native!'
'shift-down': 'native!'
'shift-right': 'native!'
'escape': 'native!'
'pageup': 'native!'
'pagedown': 'native!'
'shift-pageup': 'native!'
'shift-pagedown': 'native!'
'enter': 'native!'
'cmd-enter': 'native!'
'ctrl-enter': 'native!'
'shift-enter': 'native!'
'backspace': 'native!'
'shift-backspace': 'native!'
'delete': 'native!'
'shift-delete': 'native!'
'cmd-y': 'native!'
'cmd-z': 'native!'
'cmd-Z': 'native!'
'cmd-x': 'native!'
'cmd-X': 'native!'
'cmd-c': 'native!'
'cmd-C': 'native!'
'cmd-v': 'native!'
'cmd-V': 'native!'
'cmd-a': 'native!'
'cmd-A': 'native!'
'cmd-b': 'native!'
'cmd-i': 'native!'
'cmd-u': 'native!'
'ctrl-y': 'native!'
'ctrl-z': 'native!'
'ctrl-Z': 'native!'
'ctrl-x': 'native!'
'ctrl-X': 'native!'
'ctrl-c': 'native!'
'ctrl-C': 'native!'
'ctrl-v': 'native!'
'ctrl-V': 'native!'
'ctrl-a': 'native!'
'ctrl-A': 'native!'
'ctrl-b': 'native!'
'ctrl-i': 'native!'
'ctrl-u': 'native!'
'a': 'native!'
'b': 'native!'
'c': 'native!'
'd': 'native!'
'e': 'native!'
'f': 'native!'
'g': 'native!'
'h': 'native!'
'i': 'native!'
'j': 'native!'
'k': 'native!'
'l': 'native!'
'm': 'native!'
'n': 'native!'
'o': 'native!'
'p': 'native!'
'q': 'native!'
'r': 'native!'
's': 'native!'
't': 'native!'
'u': 'native!'
'v': 'native!'
'w': 'native!'
'x': 'native!'
'y': 'native!'
'z': 'native!'
'A': 'native!'
'B': 'native!'
'C': 'native!'
'D': 'native!'
'E': 'native!'
'F': 'native!'
'G': 'native!'
'H': 'native!'
'I': 'native!'
'J': 'native!'
'K': 'native!'
'L': 'native!'
'M': 'native!'
'N': 'native!'
'O': 'native!'
'P': 'native!'
'Q': 'native!'
'R': 'native!'
'S': 'native!'
'T': 'native!'
'U': 'native!'
'V': 'native!'
'W': 'native!'
'X': 'native!'
'Y': 'native!'
'Z': 'native!'
'1': 'native!'
'2': 'native!'
'3': 'native!'
'4': 'native!'
'5': 'native!'
'6': 'native!'
'7': 'native!'
'8': 'native!'
'9': 'native!'
'0': 'native!'
'~': 'native!'
'!': 'native!'
'@': 'native!'
'#': 'native!'
'$': 'native!'
'%': 'native!'
'^': 'native!'
'&': 'native!'
'*': 'native!'
'(': 'native!'
')': 'native!'
'-': 'native!'
'_': 'native!'
'=': 'native!'
'+': 'native!'
'[': 'native!'
'{': 'native!'
']': 'native!'
'}': 'native!'
'\\': 'native!'
'|': 'native!'
';': 'native!'
':': 'native!'
'\'': 'native!'
'"': 'native!'
'<': 'native!'
',': 'native!'
'>': 'native!'
'.': 'native!'
'?': 'native!'
'/': 'native!'
'up' : 'core:previous-item'
'down' : 'core:next-item'
'left' : 'core:move-left'
'right' : 'core:move-right'
'shift-up' : 'core:select-up'
'shift-down' : 'core:select-down'
'shift-left' : 'core:select-left'
'shift-right': 'core:select-right'
'body input, body textarea':
'tab': 'core:focus-next'
'shift-tab': 'core:focus-previous'
### Core application commands. ###
'cmdctrl-q' : 'application:quit'
'cmdctrl-w' : 'window:close'
'body webview':
'tab': 'native!'
'shift-tab': 'native!'
### Universal N1 commands. ###
'enter' : 'core:focus-item'
'delete' : 'core:remove-from-view'
'escape' : 'application:pop-sheet'
'backspace': 'core:remove-from-view'
'cmdctrl-,': 'application:open-preferences'
# So our contenteditable control can do its own thing
'body *[contenteditable]':
'tab': 'native!'
'shift-tab': 'native!'
'pageup' : 'core:messages-page-up'
'pagedown' : 'core:messages-page-down'
'shift-pageup' : 'core:list-page-up'
'shift-pagedown': 'core:list-page-down'
# For menus
'body .menu, body .menu, body .menu input':
# and by "native!" I actually mean for it to just let React deal with
# it.
'tab': 'native!'
'shift-tab': 'native!'
### N1 developer commands. ###
'cmdctrl-alt-l': 'window:reload'
'cmdctrl-alt-i': 'window:toggle-dev-tools'
'cmdctrl-alt-w': 'application:show-work-window'
'cmdctrl-alt-s': 'application:run-all-specs'
'cmdctrl-alt-p': 'application:run-package-specs'
'body *[contenteditable].contenteditable':
### Basic formatting commands ###
'cmdctrl-u': 'contenteditable:underline'
'cmdctrl-b': 'contenteditable:bold'
'cmdctrl-i': 'contenteditable:italic'
'cmdctrl-k': 'contenteditable:insert-link'
### Advanced formatting commands ###
'cmdctrl-&': 'contenteditable:numbered-list'
'cmdctrl-#': 'contenteditable:numbered-list'
'cmdctrl-*': 'contenteditable:bulleted-list'
'cmdctrl-(': 'contenteditable:quote'
'cmdctrl-[': 'contenteditable:outdent'
'cmdctrl-]': 'contenteditable:indent'
'cmdctrl-L': 'contenteditable:align-left'
'cmdctrl-E': 'contenteditable:align-center'
'cmdctrl-R': 'contenteditable:align-right'
'cmdctrl-,': 'contenteditable:set-right-to-left'
'cmdctrl-.': 'contenteditable:set-left-to-right'
'cmdctrl-\\': 'contenteditable:remove-formatting'
'cmdctrl-%': 'contenteditable:previous-font'
'cmdctrl-^': 'contenteditable:next-font'
'cmdctrl-+': 'contenteditable:increase-text-size'
'cmdctrl--': 'contenteditable:decrease-text-size'
### Custom Property Navigating ###
'cmdctrl-;': 'contenteditable:previous-selection'
"cmdctrl-'": 'contenteditable:next-selection'
'cmdctrl-m': 'contenteditable:open-spelling-suggestions'

202
keymaps/input-reset.cson Normal file
View file

@ -0,0 +1,202 @@
# We need to explicitly ensure all commands sent to inputs are handled as
# native events.
#
# When users type a key in an input, that event bubbles up. (We
# intentionally don't `stopPropagation` from inputs to allow for cases
# where you do want to catch key events.)
#
# When that keypress bubbles up to the root level, we may capture it
# thinking it's a hot key. While we could attach the special
# `.native-key-bindings` class to the input, we can't guarantee that this
# will be upheld. Furthermore, in some cases we may want to actually
# capture the input.
#
# Once captured, the event's default will be prevented.
#
# We give a higher CSS specificity to the inputs, textareas, and
# contentedtiables to ensure that the `native!` behavior takes precedent.
'body input, body textarea, body *[contenteditable], body webview':
'up': 'native!'
'left': 'native!'
'down': 'native!'
'right': 'native!'
'cmd-up': 'native!'
'cmd-left': 'native!'
'cmd-down': 'native!'
'cmd-right': 'native!'
'ctrl-up': 'native!'
'ctrl-left': 'native!'
'ctrl-down': 'native!'
'ctrl-right': 'native!'
'shift-up': 'native!'
'shift-left': 'native!'
'shift-down': 'native!'
'shift-right': 'native!'
'escape': 'native!'
'pageup': 'native!'
'pagedown': 'native!'
'shift-pageup': 'native!'
'shift-pagedown': 'native!'
'enter': 'native!'
'cmd-enter': 'native!'
'ctrl-enter': 'native!'
'shift-enter': 'native!'
'backspace': 'native!'
'shift-backspace': 'native!'
'delete': 'native!'
'shift-delete': 'native!'
'cmd-y': 'native!'
'cmd-z': 'native!'
'cmd-Z': 'native!'
'cmd-x': 'native!'
'cmd-X': 'native!'
'cmd-c': 'native!'
'cmd-C': 'native!'
'cmd-v': 'native!'
'cmd-V': 'native!'
'cmd-a': 'native!'
'cmd-A': 'native!'
'cmd-b': 'native!'
'cmd-i': 'native!'
'cmd-u': 'native!'
'ctrl-y': 'native!'
'ctrl-z': 'native!'
'ctrl-Z': 'native!'
'ctrl-x': 'native!'
'ctrl-X': 'native!'
'ctrl-c': 'native!'
'ctrl-C': 'native!'
'ctrl-v': 'native!'
'ctrl-V': 'native!'
'ctrl-a': 'native!'
'ctrl-A': 'native!'
'ctrl-b': 'native!'
'ctrl-i': 'native!'
'ctrl-u': 'native!'
'a': 'native!'
'b': 'native!'
'c': 'native!'
'd': 'native!'
'e': 'native!'
'f': 'native!'
'g': 'native!'
'h': 'native!'
'i': 'native!'
'j': 'native!'
'k': 'native!'
'l': 'native!'
'm': 'native!'
'n': 'native!'
'o': 'native!'
'p': 'native!'
'q': 'native!'
'r': 'native!'
's': 'native!'
't': 'native!'
'u': 'native!'
'v': 'native!'
'w': 'native!'
'x': 'native!'
'y': 'native!'
'z': 'native!'
'A': 'native!'
'B': 'native!'
'C': 'native!'
'D': 'native!'
'E': 'native!'
'F': 'native!'
'G': 'native!'
'H': 'native!'
'I': 'native!'
'J': 'native!'
'K': 'native!'
'L': 'native!'
'M': 'native!'
'N': 'native!'
'O': 'native!'
'P': 'native!'
'Q': 'native!'
'R': 'native!'
'S': 'native!'
'T': 'native!'
'U': 'native!'
'V': 'native!'
'W': 'native!'
'X': 'native!'
'Y': 'native!'
'Z': 'native!'
'1': 'native!'
'2': 'native!'
'3': 'native!'
'4': 'native!'
'5': 'native!'
'6': 'native!'
'7': 'native!'
'8': 'native!'
'9': 'native!'
'0': 'native!'
'~': 'native!'
'`': 'native!'
'!': 'native!'
'@': 'native!'
'#': 'native!'
'$': 'native!'
'%': 'native!'
'^': 'native!'
'&': 'native!'
'*': 'native!'
'(': 'native!'
')': 'native!'
'-': 'native!'
'_': 'native!'
'=': 'native!'
'+': 'native!'
'[': 'native!'
'{': 'native!'
']': 'native!'
'}': 'native!'
'\\': 'native!'
'|': 'native!'
';': 'native!'
':': 'native!'
'\'': 'native!'
'"': 'native!'
'<': 'native!'
',': 'native!'
'>': 'native!'
'.': 'native!'
'?': 'native!'
'/': 'native!'
'g i': 'native!'
'g s': 'native!'
'g t': 'native!'
'g d': 'native!'
'g a': 'native!'
'g c': 'native!'
'g k': 'native!'
'g l': 'native!'
'* a': 'native!'
'* n': 'native!'
'* r': 'native!'
'* u': 'native!'
'* s': 'native!'
'* t': 'native!'
# Tabs are a bit different because simple elements (like text inputs) we
# want to use our custom `core:focus-next`. Other more complex ones, like
# `contenteditable`, we want to have a more controlled effect over.
'body input, body textarea':
'tab': 'core:focus-next'
'shift-tab': 'core:focus-previous'
# So our contenteditable control can do its own thing
'body webview, body *[contenteditable]':
'tab': 'native!'
'shift-tab': 'native!'
# For menus
'body .menu, body .menu, body .menu input':
# and by "native!" I actually mean for it to just let React deal with
# it.
'tab': 'native!'
'shift-tab': 'native!'

View file

@ -1,22 +1,12 @@
# Note: For a menu item to have a keyboard equiavalent, it needs
# to be listed in this file.
'body.platform-darwin':
'cmd-n' : 'application:new-message'
'cmd-r' : 'application:reply'
'cmd-R' : 'application:reply-all'
'cmd-F' : 'application:forward'
'cmd-alt-f': 'application:focus-search'
'cmd-D': 'application:send-message'
'cmd-V': 'application:change-category'
'cmd-e' : 'core:archive-item'
'body.platform-linux, body.platform-win32':
'ctrl-n' : 'application:new-message'
'ctrl-r' : 'application:reply'
'ctrl-R' : 'application:reply-all'
'ctrl-F' : 'application:forward'
'ctrl-alt-f': 'application:focus-search'
'ctrl-D': 'application:send-message'
'ctrl-shift-v': 'application:change-category'
'ctrl-e' : 'core:archive-item'
'body':
'cmdctrl-n' : 'application:new-message'
'cmdctrl-r' : 'application:reply'
'cmdctrl-R' : 'application:reply-all'
'cmdctrl-F' : 'application:forward'
'cmdctrl-alt-f': 'application:focus-search'
'cmdctrl-D': 'application:send-message'
'cmdctrl-V': 'application:change-category'
'cmdctrl-e' : 'application:archive-item'

View file

@ -1,38 +1,83 @@
# Email-specific core key-mappings
#
# There are additional mappings in <platform>.cson files that bind
# menu items. In the future, we should break these into files like:
# darwin-gmail.cson, darwin-macmail.cson, win32-gmail.cson...
# Gmail-specific core key-mappings
'body':
'c' : 'application:new-message'
'/' : 'application:focus-search'
'r' : 'application:reply'
'a' : 'application:reply-all'
'f' : 'application:forward'
'l' : 'application:change-category'
'u' : 'application:pop-sheet'
### Jumping ###
'g i' : 'navigation:go-to-inbox'
'g s' : 'navigation:go-to-starred'
'g t' : 'navigation:go-to-sent'
'g d' : 'navigation:go-to-drafts'
'g a' : 'navigation:go-to-all'
'g c' : 'navigation:go-to-contacts'
'g k' : 'navigation:go-to-tasks'
'g l' : 'navigation:go-to-label'
'k' : 'core:previous-item'
'j' : 'core:next-item'
']' : 'core:remove-and-previous'
'[' : 'core:remove-and-next'
'#' : 'core:delete-item'
'e' : 'core:archive-item'
's' : 'core:star-item'
'x' : 'core:select-item'
### Threadlist selection ###
'* a': 'thread-list:select-all'
'* n': 'thread-list:deselect-all'
'* r': 'thread-list:select-read'
'* u': 'thread-list:select-unread'
'* s': 'thread-list:select-starred'
'* t': 'thread-list:select-unstarred'
# Gmail also includes some more basic ones that users expect from desktop software.
### Navigation ###
'u': 'application:pop-sheet'
'k': 'core:previous-item'
'j': 'core:next-item'
'o': 'core:focus-item'
'p': 'message-list:previous-message'
'n': 'message-list:next-message'
'`': 'thread-list:next-inbox'
'~': 'thread-list:previous-inbox'
'body.platform-darwin':
'cmd-n' : 'application:new-message'
'cmd-r' : 'application:reply'
'cmd-R' : 'application:reply-all'
'cmd-F' : 'application:forward'
### Application ###
'c': 'application:new-message'
'd': 'application:new-message'
'/': 'application:focus-search'
'.': 'application:more-actions'
'l': 'application:change-category'
'v': 'application:change-category'
'?': 'application:open-help'
'body.platform-linux, body.platform-win32':
'ctrl-n' : 'application:new-message'
'ctrl-r' : 'application:reply'
'ctrl-R' : 'application:reply-all'
'ctrl-F' : 'application:forward'
### Actions ###
',': 'application:focus-toolbar'
'x': 'core:select-item'
's': 'application:star-item'
'y': 'core:remove-from-view' # Remove label
'e': 'application:archive-item'
'm': 'application:mute-conversation'
'!': 'application:report-as-spam'
'#': 'application:delete-item'
'r': 'application:reply'
'R': 'application:reply-new-window'
'a': 'application:reply-all'
'A': 'application:reply-all-new-window'
'f': 'application:forward'
'F': 'application:forward-new-window'
'N': 'application:update-conversation'
']': 'application:remove-and-previous'
'}': 'application:remove-and-previous'
'[': 'application:remove-and-next'
'{': 'application:remove-and-next'
'z': 'core:undo'
'I': 'application:mark-as-read'
'U': 'application:mark-as-unread'
'_': 'application:mark-as-unread' # Mark unread from the selected message
'+': 'application:mark-important'
'=': 'application:mark-important'
'-': 'application:mark-unimportant'
';': 'message-list:expand-all'
':': 'message-list:collapse-all'
# While not stock Gmail, we add the following standard keymaps that
# desktop application users would expect:
'cmdctrl-n' : 'application:new-message'
'cmdctrl-r' : 'application:reply'
'cmdctrl-R' : 'application:reply-all'
'cmdctrl-F' : 'application:forward'

View file

@ -1,34 +1,18 @@
# Note: For a menu item to have a keyboard equiavalent, it needs
# to be listed in this file.
'body.platform-darwin':
'body':
# Windows email-specific menu items
'cmd-shift-v': 'application:change-category' # Outlook
'cmdctrl-shift-v': 'application:change-category' # Outlook
'F3': 'application:focus-search'
'cmd-e': 'application:focus-search'
'cmd-f': 'application:forward'
'cmd-shift-v': 'application:change-category'
'cmd-d': 'core:delete-item'
'cmdctrl-e': 'application:focus-search'
'cmdctrl-f': 'application:forward'
'cmdctrl-shift-v': 'application:change-category'
'cmdctrl-d': 'application:delete-item'
'alt-backspace':'core:undo'
'alt-s': 'application:send-message'
'cmd-r': 'application:reply'
'cmd-shift-r': 'application:reply-all'
'cmd-n' : 'application:new-message'
'cmd-shift-m': 'application:new-message'
'cmd-enter': 'send'
'body.platform-linux, body.platform-win32':
# Windows email-specific menu items
'ctrl-shift-v': 'application:change-category' # Outlook
'F3': 'application:focus-search'
'ctrl-e': 'application:focus-search'
'ctrl-f': 'application:forward'
'ctrl-shift-v': 'application:change-category'
'ctrl-d': 'core:delete-item'
'alt-backspace':'core:undo'
'alt-s': 'application:send-message'
'ctrl-r': 'application:reply'
'ctrl-shift-r': 'application:reply-all'
'ctrl-n' : 'application:new-message'
'ctrl-shift-m': 'application:new-message'
'ctrl-enter': 'send'
'cmdctrl-r': 'application:reply'
'cmdctrl-shift-r': 'application:reply-all'
'cmdctrl-n' : 'application:new-message'
'cmdctrl-shift-m': 'application:new-message'
'cmdctrl-enter': 'send'

View file

@ -15,7 +15,7 @@
"dependencies": {
"asar": "^0.5.0",
"async": "^0.9",
"atom-keymap": "^5.1",
"atom-keymap": "6.1.0",
"babel-core": "^6.0.20",
"babel-preset-es2015": "^6.0.15",
"babel-preset-react": "^6.0.15",

View file

@ -6,16 +6,14 @@ NylasTestUtils =
loadKeymap: (keymapPath) ->
{resourcePath} = atom.getLoadSettings()
basePath = CSON.resolve("#{resourcePath}/keymaps/base")
baseKeymaps = CSON.readFileSync(basePath)
atom.keymaps.add(basePath, baseKeymaps)
atom.keymaps.loadKeymap(basePath)
if keymapPath?
keymapPath = CSON.resolve("#{resourcePath}/#{keymapPath}")
keymapFile = CSON.readFileSync(keymapPath)
atom.keymaps.add(keymapPath, keymapFile)
atom.keymaps.loadKeymap(keymapPath)
keyPress: (key, target) ->
event = KeymapManager.buildKeydownEvent(key, target: target)
document.dispatchEvent(event)
atom.keymaps.handleKeyboardEvent(event)
module.exports = NylasTestUtils

View file

@ -9,7 +9,7 @@ _ = require 'underscore'
_str = require 'underscore.string'
fs = require 'fs-plus'
Grim = require 'grim'
KeymapManager = require '../src/keymap-extensions'
KeymapManager = require '../src/keymap-manager'
# FIXME: Remove jquery from this
{$} = require '../src/space-pen-extensions'

View file

@ -151,7 +151,7 @@ class Atom extends Model
@loadTime = null
Config = require './config'
KeymapManager = require './keymap-extensions'
KeymapManager = require './keymap-manager'
CommandRegistry = require './command-registry'
PackageManager = require './package-manager'
Clipboard = require './clipboard'
@ -183,11 +183,11 @@ class Atom extends Model
@config = new Config({configDirPath, resourcePath})
@keymaps = new KeymapManager({configDirPath, resourcePath})
@keymaps.subscribeToFileReadFailure()
@keymaps.onDidMatchBinding (event) ->
# If the user fired a command with the application: prefix bound to the body, re-fire it
# up into the browser process. This prevents us from needing this crap, which has to be
# updated every time a new application: command is added:
# If the user fired a command with the application: prefix bound to
# the body, re-fire it up into the browser process. This prevents us
# from needing this crap, which has to be updated every time a new
# application: command is added:
# https://github.com/atom/atom/blob/master/src/workspace-element.coffee#L119
if event.binding.command.indexOf('application:') is 0 and event.binding.selector.indexOf("body") is 0
ipc.send('command', event.binding.command)

View file

@ -0,0 +1,137 @@
_ = require 'underscore'
React = require 'react'
###
Public: Easily respond to keyboard shortcuts
A keyboard shortcut has two parts to it:
1. A mapping between keyboard actions and a command
2. A mapping between a command and a callback handler
## Mapping keys to commands (not handled by this component)
The **keyboard -> command** mapping is defined in a separate `.cson` file.
A majority of the commands your component would want to listen to you have
already been defined by core N1 defaults, as well as custom user
overrides. See 'keymaps/base.cson' for more information.
You can define additional, custom keyboard -> command mappings in your own
package-specific keymap `.cson` file. The file can be named anything but
must exist in a folder called `keymaps` in the root of your package's
directory.
## Mapping commands to callbacks (handled by this component)
When a keystroke sequence matches a binding in a given context, a custom
DOM event with a type based on the command is dispatched on the target of
the keyboard event.
That custom DOM event (whose type is the command you want to listen to)
will propagate up from its original target. That original target may or
may not be a descendent of your <KeyCommandsRegion> component.
Frequently components will want to listen to a keyboard command regardless
of where it was fired from. For those, use the `globalHandlers` prop. The
DOM event will NOT be passed to `globalHandlers` callbacks.
Components may also want to listen to keyboard commands that originate
within one of their descendents. For those use the `localHandlers` prop.
The DOM event WILL be passed to `localHandlers` callback because it is
sometimes valuable to call `stopPropagataion` on the custom command event.
Props:
- `localHandlers` A mapping between key commands and callbacks for key command events that originate within a descendent of this component.
- `globalHandlers` A mapping between key commands and callbacks for key
commands that originate from anywhere and are global in scope.
- `className` The unique class name that shows up in your keymap.cson
Example:
In `my-package/lib/my-component.cjsx`:
```coffee
class MyComponent extends React.Component
render: ->
<KeyCommandsRegion globalHandlers={@globalHandlers()} className="my-component">
<div>... sweet component ...</div>
</KeyCommandsRegion>
globalHandlers: ->
"core:moveDown": @onMoveDown
"core:selectItem": @onSelectItem
localHandlers: ->
"custom:send": (event) => @onSelectItem(); event.stopPropagation()
"custom:move": @onCustomMove
```
In `my-package/keymaps/my-package.cson`:
```coffee
".my-component":
"cmd-t": "selectItem"
"cmd-enter": "sendMessage"
```
###
class KeyCommandsRegion extends React.Component
@displayName: "KeyCommandsRegion"
@propTypes:
className: React.PropTypes.string
localHandlers: React.PropTypes.object
globalHandlers: React.PropTypes.object
@defaultProps:
className: ""
localHandlers: {}
globalHandlers: {}
componentWillReceiveProps: (newProps) ->
@_unmountListeners()
@_setupListeners(newProps)
componentDidMount: ->
@_mounted = true
@_setupListeners(@props)
componentWillUnmount: ->
@_unmountListeners()
# When the {KeymapManager} finds a valid keymap in a `.cson` file, it
# will create a CustomEvent with the command name as its type. That
# custom event will be fired at the originating target and propogate
# updwards until it reaches the root window level.
#
# An event is scoped in the `.cson` files. Since we use that to
# determine which keymappings can fire a particular command in a
# particular scope, we simply need to listen at the root window level
# here for all commands coming in.
_setupListeners: (props) ->
_.each props.globalHandlers, (callback, handler) ->
window.addEventListener(handler, callback)
return unless @_mounted
$el = React.findDOMNode(@)
_.each props.localHandlers, (callback, handler) ->
$el.addEventListener(handler, callback)
_unmountListeners: ->
_.each @props.globalHandlers, (callback, handler) ->
window.removeEventListener(handler, callback)
return unless @_mounted
$el = React.findDOMNode(@)
_.each @props.localHandlers, (callback, handler) ->
$el.removeEventListener(handler, callback)
render: ->
<div className="key-commands-region #{@props.className}">
{@props.children}
</div>
module.exports = KeyCommandsRegion

View file

@ -8,6 +8,7 @@ Spinner = require './spinner'
WorkspaceStore,
FocusedContentStore,
AccountStore} = require 'nylas-exports'
{KeyCommandsRegion} = require 'nylas-component-kit'
EventEmitter = require('events').EventEmitter
MultiselectListInteractionHandler = require './multiselect-list-interaction-handler'
@ -30,7 +31,6 @@ class MultiselectList extends React.Component
@propTypes =
className: React.PropTypes.string.isRequired
collection: React.PropTypes.string.isRequired
commands: React.PropTypes.object.isRequired
columns: React.PropTypes.array.isRequired
dataStore: React.PropTypes.object.isRequired
itemPropsProvider: React.PropTypes.func.isRequired
@ -66,30 +66,23 @@ class MultiselectList extends React.Component
teardownForProps: =>
return unless @unsubscribers
unsubscribe() for unsubscribe in @unsubscribers
@command_unsubscriber.dispose()
setupForProps: (props) =>
commands = _.extend {},
'core:focus-item': => @_onEnter()
'core:select-item': => @_onSelect()
'core:next-item': => @_onShift(1)
'core:previous-item': => @_onShift(-1)
'core:select-down': => @_onShift(1, {select: true})
'core:select-up': => @_onShift(-1, {select: true})
'core:list-page-up': => @_onScrollByPage(-1)
'core:list-page-down': => @_onScrollByPage(1)
'application:pop-sheet': => @_onDeselect()
Object.keys(props.commands).forEach (key) =>
commands[key] = =>
context = {focusedId: @state.focusedId}
props.commands[key](context)
@unsubscribers = []
@unsubscribers.push props.dataStore.listen @_onChange
@unsubscribers.push WorkspaceStore.listen @_onChange
@unsubscribers.push FocusedContentStore.listen @_onChange
@command_unsubscriber = atom.commands.add('body', commands)
_keymapHandlers: ->
'core:focus-item': => @_onEnter()
'core:select-item': => @_onSelect()
'core:next-item': => @_onShift(1)
'core:previous-item': => @_onShift(-1)
'core:select-down': => @_onShift(1, {select: true})
'core:select-up': => @_onShift(-1, {select: true})
'core:list-page-up': => @_onScrollByPage(-1)
'core:list-page-down': => @_onScrollByPage(1)
'application:pop-sheet': => @_onDeselect()
render: =>
# IMPORTANT: DO NOT pass inline functions as props. _.isEqual thinks these
@ -122,19 +115,21 @@ class MultiselectList extends React.Component
spinnerElement = <Spinner visible={!@state.loaded and @state.empty} />
<div className={className} {...otherProps}>
<ListTabular
ref="list"
columns={@state.computedColumns}
scrollTooltipComponent={@props.scrollTooltipComponent}
dataView={@state.dataView}
itemPropsProvider={@itemPropsProvider}
itemHeight={@props.itemHeight}
onSelect={@_onClickItem}
onDoubleClick={@props.onDoubleClick} />
{spinnerElement}
{emptyElement}
</div>
<KeyCommandsRegion globalHandlers={@_keymapHandlers()} className="multiselect-list">
<div className={className} {...otherProps}>
<ListTabular
ref="list"
columns={@state.computedColumns}
scrollTooltipComponent={@props.scrollTooltipComponent}
dataView={@state.dataView}
itemPropsProvider={@itemPropsProvider}
itemHeight={@props.itemHeight}
onSelect={@_onClickItem}
onDoubleClick={@props.onDoubleClick} />
{spinnerElement}
{emptyElement}
</div>
</KeyCommandsRegion>
else
<div className={className} {...otherProps}>
<Spinner visible={true} />

View file

@ -1,4 +1,5 @@
NylasStore = require 'nylas-store'
WorkspaceStore = require './workspace-store'
MailViewFilter = require '../../mail-view-filter'
CategoryStore = require './category-store'
AccountStore = require './account-store'
@ -18,8 +19,9 @@ class FocusedMailViewStore extends NylasStore
else if not CategoryStore.byId(@_mailView.categoryId())
@_setMailView(@_defaultMailView())
_onFocusMailView: (filter) ->
_onFocusMailView: (filter) =>
return if filter.isEqual(@_mailView)
Actions.selectRootSheet(WorkspaceStore.Sheet.Threads)
Actions.searchQueryCommitted('')
@_setMailView(filter)
@ -36,7 +38,7 @@ class FocusedMailViewStore extends NylasStore
@_mailViewBeforeSearch = null
_defaultMailView: ->
category = CategoryStore.getStandardCategory('inbox')
category = CategoryStore.getStandardCategory("inbox")
return null unless category
MailViewFilter.forCategory(category)

View file

@ -1,6 +1,8 @@
_ = require 'underscore'
Actions = require '../actions'
AccountStore = require './account-store'
CategoryStore = require './category-store'
MailViewFilter = require '../../mail-view-filter'
NylasStore = require 'nylas-store'
Sheet = {}
@ -41,8 +43,38 @@ class WorkspaceStore extends NylasStore
@popToRootSheet()
@trigger()
atom.commands.add 'body',
'application:pop-sheet': => @popSheet()
atom.commands.add 'body', @_navigationCommands()
_navigationCommands: ->
'application:pop-sheet' : => @popSheet()
'navigation:go-to-inbox' : => @_setMailViewByName("inbox")
'navigation:go-to-starred' : => @_selectStarredView()
'navigation:go-to-sent' : => @_setMailViewByName("sent")
'navigation:go-to-drafts' : => @_selectDraftsSheet()
'navigation:go-to-all' : => @_selectAllView()
'navigation:go-to-contacts': => ## TODO
'navigation:go-to-tasks' : => ## TODO
'navigation:go-to-label' : => ## TODO
_setMailViewByName: (categoryName) ->
category = CategoryStore.getStandardCategory(categoryName)
return unless category
view = MailViewFilter.forCategory(category)
return unless view
Actions.focusMailView(view)
_selectDraftsSheet: ->
Actions.selectRootSheet(@Sheet.Drafts)
_selectAllView: ->
category = CategoryStore.getArchiveCategory()
return unless category
view = MailViewFilter.forCategory(category)
return unless view
Actions.focusMailView(view)
_selectStarredView: ->
Actions.focusMailView MailViewFilter.forStarred()
_resetInstanceVars: =>
@Location = Location = {}

View file

@ -23,6 +23,7 @@ class NylasComponentKit
@load "ButtonDropdown", 'button-dropdown'
@load "Contenteditable", 'contenteditable/contenteditable'
@load "MultiselectList", 'multiselect-list'
@load "KeyCommandsRegion", 'key-commands-region'
@load "InjectedComponent", 'injected-component'
@load "TokenizingTextField", 'tokenizing-text-field'
@load "MultiselectActionBar", 'multiselect-action-bar'

View file

@ -146,6 +146,6 @@ class NylasExports
@get "APMWrapper", -> require('../apm-wrapper')
# Testing
@get "NylasTestUtils", -> require '../../spec/test_utils'
@get "NylasTestUtils", -> require '../../spec/nylas-test-utils'
module.exports = NylasExports

View file

@ -1,83 +0,0 @@
fs = require 'fs-plus'
path = require 'path'
KeymapManager = require 'atom-keymap'
CSON = require 'season'
{jQuery} = require 'space-pen'
Grim = require 'grim'
KeymapManager::onDidLoadBundledKeymaps = (callback) ->
@emitter.on 'did-load-bundled-keymaps', callback
KeymapManager::loadBundledKeymaps = ->
# Load the base keymap and the base.platform keymap
baseKeymap = fs.resolve(path.join(@resourcePath, 'keymaps'), 'base', ['cson', 'json'])
basePlatformKeymap = fs.resolve(path.join(@resourcePath, 'keymaps'), "base-#{process.platform}", ['cson', 'json'])
@loadKeymap(baseKeymap)
@loadKeymap(basePlatformKeymap)
# Load the template keymap (Gmail, Mail.app, etc.) the user has chosen
templateConfigKey = 'core.keymapTemplate'
templateKeymapPath = null
reloadTemplateKeymap = =>
@removeBindingsFromSource(templateKeymapPath) if templateKeymapPath
templateFile = atom.config.get(templateConfigKey)
if templateFile
templateKeymapPath = fs.resolve(path.join(@resourcePath, 'keymaps', 'templates'), templateFile, ['cson', 'json'])
if fs.existsSync(templateKeymapPath)
@loadKeymap(templateKeymapPath)
@emitter.emit('did-reload-keymap', {path: templateKeymapPath})
else
console.warn("Could not find #{templateKeymapPath}")
atom.config.observe(templateConfigKey, reloadTemplateKeymap)
reloadTemplateKeymap()
@emit 'bundled-keymaps-loaded' if Grim.includeDeprecatedAPIs
@emitter.emit 'did-load-bundled-keymaps'
KeymapManager::getUserKeymapPath = ->
if userKeymapPath = CSON.resolve(path.join(@configDirPath, 'keymap'))
userKeymapPath
else
path.join(@configDirPath, 'keymap.cson')
KeymapManager::loadUserKeymap = ->
userKeymapPath = @getUserKeymapPath()
return unless fs.isFileSync(userKeymapPath)
try
@loadKeymap(userKeymapPath, watch: true, suppressErrors: true)
catch error
if error.message.indexOf('Unable to watch path') > -1
message = """
Unable to watch path: `#{path.basename(userKeymapPath)}`. Make sure you
have permission to read `#{userKeymapPath}`.
On linux there are currently problems with watch sizes. See
[this document][watches] for more info.
[watches]:https://github.com/atom/atom/blob/master/docs/build-instructions/linux.md#typeerror-unable-to-watch-path
"""
console.error(message, {dismissable: true})
else
detail = error.path
stack = error.stack
atom.notifications.addFatalError(error.message, {detail, stack, dismissable: true})
KeymapManager::subscribeToFileReadFailure = ->
@onDidFailToReadFile (error) =>
userKeymapPath = @getUserKeymapPath()
message = "Failed to load `#{userKeymapPath}`"
detail = if error.location?
error.stack
else
error.message
console.error(message, {detail: detail, dismissable: true})
# This enables command handlers registered via jQuery to call
# `.abortKeyBinding()` on the `jQuery.Event` object passed to the handler.
jQuery.Event::abortKeyBinding = ->
@originalEvent?.abortKeyBinding?()
module.exports = KeymapManager

97
src/keymap-manager.coffee Normal file
View file

@ -0,0 +1,97 @@
fs = require 'fs-plus'
path = require 'path'
CSON = require 'season'
AtomKeymap = require 'atom-keymap'
class KeymapManager extends AtomKeymap
constructor: ->
super
@subscribeToFileReadFailure()
onDidLoadBundledKeymaps: (callback) ->
@emitter.on 'did-load-bundled-keymaps', callback
# N1 adds the `cmdctrl` extension. This will use `cmd` or `ctrl` on a
# mac, and `ctrl` only on windows and linux.
readKeymap: (args...) ->
re = /(cmdctrl|ctrlcmd)/i
keymap = super(args...)
for selector, keyBindings of keymap
normalizedBindings = {}
for keystrokes, command of keyBindings
if re.test keystrokes
if process.platform is "darwin"
newKeystrokes1= keystrokes.replace(re, "ctrl")
newKeystrokes2= keystrokes.replace(re, "cmd")
normalizedBindings[newKeystrokes1] = command
normalizedBindings[newKeystrokes2] = command
else
newKeystrokes = keystrokes.replace(re, "ctrl")
normalizedBindings[newKeystrokes] = command
else
normalizedBindings[keystrokes] = command
keymap[selector] = normalizedBindings
return keymap
loadBundledKeymaps: ->
# Load the base keymap and the base.platform keymap
baseKeymap = fs.resolve(path.join(@resourcePath, 'keymaps'), 'base', ['cson', 'json'])
inputResetKeymap = fs.resolve(path.join(@resourcePath, 'keymaps'), 'input-reset', ['cson', 'json'])
basePlatformKeymap = fs.resolve(path.join(@resourcePath, 'keymaps'), "base-#{process.platform}", ['cson', 'json'])
@loadKeymap(baseKeymap)
@loadKeymap(inputResetKeymap)
@loadKeymap(basePlatformKeymap)
# Load the template keymap (Gmail, Mail.app, etc.) the user has chosen
templateConfigKey = 'core.keymapTemplate'
templateKeymapPath = null
reloadTemplateKeymap = =>
@removeBindingsFromSource(templateKeymapPath) if templateKeymapPath
templateFile = atom.config.get(templateConfigKey)
if templateFile
templateKeymapPath = fs.resolve(path.join(@resourcePath, 'keymaps', 'templates'), templateFile, ['cson', 'json'])
if fs.existsSync(templateKeymapPath)
@loadKeymap(templateKeymapPath)
@emitter.emit('did-reload-keymap', {path: templateKeymapPath})
else
console.warn("Could not find #{templateKeymapPath}")
atom.config.observe(templateConfigKey, reloadTemplateKeymap)
reloadTemplateKeymap()
@emitter.emit 'did-load-bundled-keymaps'
getUserKeymapPath: ->
if userKeymapPath = CSON.resolve(path.join(@configDirPath, 'keymap'))
userKeymapPath
else
path.join(@configDirPath, 'keymap.cson')
loadUserKeymap: ->
userKeymapPath = @getUserKeymapPath()
return unless fs.isFileSync(userKeymapPath)
try
@loadKeymap(userKeymapPath, watch: true, suppressErrors: true)
catch error
message = """
Unable to watch path: `#{path.basename(userKeymapPath)}`. Make sure you
have permission to read `#{userKeymapPath}`.
"""
console.error(message, {dismissable: true})
subscribeToFileReadFailure: ->
@onDidFailToReadFile (error) =>
userKeymapPath = @getUserKeymapPath()
message = "Failed to load `#{userKeymapPath}`"
detail = if error.location?
error.stack
else
error.message
console.error(message, {detail: detail, dismissable: true})
module.exports = KeymapManager

View file

@ -21,6 +21,9 @@ class MailViewFilter
@forSearch: (query) ->
new SearchMailViewFilter(query)
@forAll: ->
new AllMailViewFilter()
# Instance Methods
constructor: ->
@ -83,6 +86,31 @@ class SearchMailViewFilter extends MailViewFilter
categoryId: ->
null
class AllMailViewFilter extends MailViewFilter
constructor: ->
@name = "All"
@iconName = "all-mail.png"
@
isEqual: (other) ->
super(other) and other.searchQuery is @searchQuery
matchers: ->
account = AccountStore.current()
[Thread.attributes.accountId.equal(account.id)]
canApplyToThreads: ->
true
canArchiveThreads: ->
false
canTrashThreads: ->
false
categoryId: ->
CategoryStore.getStandardCategory("all")?.id
class StarredMailViewFilter extends MailViewFilter
constructor: ->

View file

@ -245,7 +245,7 @@ class Package
if @bundledPackage and packagesCache[@name]?
@keymaps = (["#{atom.packages.resourcePath}#{path.sep}#{keymapPath}", keymapObject] for keymapPath, keymapObject of packagesCache[@name].keymaps)
else
@keymaps = @getKeymapPaths().map (keymapPath) -> [keymapPath, CSON.readFileSync(keymapPath) ? {}]
@keymaps = @getKeymapPaths().map (keymapPath) -> [keymapPath, atom.keymaps.readKeymap(keymapPath) ? {}]
loadMenus: ->
if @bundledPackage and packagesCache[@name]?

View file

@ -0,0 +1,5 @@
.key-commands-region {
position: relative;
height: 100%;
width: 100%;
}

View file

@ -25,3 +25,4 @@
@import "components/spinner";
@import "components/generated-form";
@import "components/unsafe";
@import "components/key-commands-region";