mirror of
https://github.com/Foundry376/Mailspring.git
synced 2025-01-01 13:14:16 +08:00
feat(*): draft icon, misc fixes, and WorkspaceStore / custom toolbar in secondary windows
Summary: Features: - ThreadListParticipants ignores drafts when computing participants, renders "Draft" label, pending design - Put the WorkspaceStore in every window—means they all get toolbars and custom gumdrop icons on Mac OS X Bug Fixes: - Never display notifications for email the user just sent - Fix obscure issue with DatabaseView trying to update metadata on items it froze. This resolves issue with names remaining bold after marking as read, drafts not appearing in message list immediately. - When you pop out a draft, save it first and *wait* for the commit() promise to succeed. - If you scroll very fast, you node.contentWindow can be null in eventedIframe Other: Make it OK to re-register the same component Make it possible to unregister a hot window Break the Sheet Toolbar out into it's own file to make things manageable Replace `package.windowPropsReceived` with a store-style model where anyone can listen for changes to `windowProps` When I put the WorkspaceStore in every window, I ran into a problem because the package was no longer rendering an instance of the Composer, it was declaring a root sheet with a composer in it. This meant that it was actually a React component that needed to listen to window props, not the package itself. `atom` is already an event emitter, so I added a `onWindowPropsReceived` hook so that components can listen to window props as if they were listening to a store. I think this might be more flexible than only broadcasting the props change event to packages. Test Plan: Run tests Reviewers: evan Reviewed By: evan Differential Revision: https://phab.nylas.com/D1592
This commit is contained in:
parent
9236a2289b
commit
89e9cdef8d
29 changed files with 421 additions and 286 deletions
|
@ -2,8 +2,8 @@ React = require 'react'
|
|||
{Message, Actions, NamespaceStore} = require 'nylas-exports'
|
||||
{RetinaImg} = require 'nylas-component-kit'
|
||||
|
||||
class NewComposeButton extends React.Component
|
||||
@displayName: 'NewComposeButton'
|
||||
class ComposeButton extends React.Component
|
||||
@displayName: 'ComposeButton'
|
||||
|
||||
render: =>
|
||||
<button style={order: 101}
|
||||
|
@ -15,4 +15,4 @@ class NewComposeButton extends React.Component
|
|||
|
||||
_onNewCompose: => Actions.composeNewBlankDraft()
|
||||
|
||||
module.exports = NewComposeButton
|
||||
module.exports = ComposeButton
|
|
@ -342,8 +342,6 @@ class ComposerView extends React.Component
|
|||
@_saveToHistory(selections) unless source.fromUndoManager
|
||||
|
||||
_popoutComposer: =>
|
||||
return unless @_proxy
|
||||
@_proxy.changes.commit()
|
||||
Actions.composePopoutDraft @props.localId
|
||||
|
||||
_sendDraft: (options = {}) =>
|
||||
|
|
|
@ -6,47 +6,30 @@ React = require 'react'
|
|||
Message,
|
||||
ComponentRegistry,
|
||||
WorkspaceStore} = require('nylas-exports')
|
||||
NewComposeButton = require('./new-compose-button')
|
||||
ComposeButton = require('./compose-button')
|
||||
ComposerView = require('./composer-view')
|
||||
|
||||
module.exports =
|
||||
|
||||
activate: (@state={}) ->
|
||||
atom.registerHotWindow
|
||||
windowType: "composer"
|
||||
replenishNum: 2
|
||||
class ComposerWithWindowProps extends React.Component
|
||||
@displayName: 'ComposerWithWindowProps'
|
||||
@containerRequired: false
|
||||
|
||||
# Register our composer as the app-wide Composer
|
||||
ComponentRegistry.register ComposerView,
|
||||
role: 'Composer'
|
||||
constructor: (@props) ->
|
||||
@state = atom.getWindowProps()
|
||||
|
||||
if atom.isMainWindow()
|
||||
ComponentRegistry.register NewComposeButton,
|
||||
location: WorkspaceStore.Location.RootSidebar.Toolbar
|
||||
else
|
||||
@_setupContainer()
|
||||
componentDidMount: ->
|
||||
@unlisten = atom.onWindowPropsReceived (windowProps) =>
|
||||
{errorMessage} = windowProps
|
||||
@_showInitialErrorDialog(errorMessage) if errorMessage
|
||||
@setState(windowProps)
|
||||
|
||||
windowPropsReceived: ({draftLocalId, errorMessage}) ->
|
||||
return unless @_container
|
||||
React.render(
|
||||
<ComposerView mode="fullwindow" localId={draftLocalId} />, @_container
|
||||
)
|
||||
if errorMessage
|
||||
@_showInitialErrorDialog(errorMessage)
|
||||
componentWillUnmount: ->
|
||||
@unlisten()
|
||||
|
||||
deactivate: ->
|
||||
ComponentRegistry.unregister(ComposerView)
|
||||
if atom.isMainWindow()
|
||||
ComponentRegistry.unregister(NewComposeButton)
|
||||
|
||||
serialize: -> @state
|
||||
|
||||
_setupContainer: ->
|
||||
if @_container? then return # Activate once
|
||||
@_container = document.createElement("div")
|
||||
@_container.setAttribute("id", "composer-full-window")
|
||||
@_container.setAttribute("class", "composer-full-window")
|
||||
document.body.appendChild(@_container)
|
||||
render: ->
|
||||
<div className="composer-full-window">
|
||||
<ComposerView mode="fullwindow" localId={@state.draftLocalId} />
|
||||
</div>
|
||||
|
||||
_showInitialErrorDialog: (msg) ->
|
||||
remote = require('remote')
|
||||
|
@ -57,3 +40,30 @@ module.exports =
|
|||
message: "Error"
|
||||
detail: msg
|
||||
}
|
||||
|
||||
module.exports =
|
||||
activate: (@state={}) ->
|
||||
atom.registerHotWindow
|
||||
windowType: 'composer'
|
||||
replenishNum: 2
|
||||
|
||||
# Register our composer as the app-wide Composer
|
||||
ComponentRegistry.register ComposerView,
|
||||
role: 'Composer'
|
||||
|
||||
if atom.isMainWindow()
|
||||
ComponentRegistry.register ComposeButton,
|
||||
location: WorkspaceStore.Location.RootSidebar.Toolbar
|
||||
else
|
||||
WorkspaceStore.defineSheet 'Main', {root: true},
|
||||
list: ['Center']
|
||||
ComponentRegistry.register ComposerWithWindowProps,
|
||||
location: WorkspaceStore.Location.Center
|
||||
|
||||
deactivate: ->
|
||||
atom.unregisterHotWindow('composer')
|
||||
ComponentRegistry.unregister(ComposerView)
|
||||
ComponentRegistry.unregister(ComposeButton)
|
||||
ComponentRegistry.unregister(ComposerWithWindowProps)
|
||||
|
||||
serialize: -> @state
|
||||
|
|
|
@ -108,7 +108,8 @@
|
|||
padding: 5px @spacing-standard 0 0;
|
||||
margin: 0 @spacing-standard;
|
||||
border-bottom: 1px solid @border-color-divider;
|
||||
|
||||
flex-shrink:0;
|
||||
|
||||
.subject-label {
|
||||
color: @text-color-very-subtle;
|
||||
float: left;
|
||||
|
@ -280,6 +281,7 @@ body.is-blurred .composer-inner-wrap .tokenizing-field .token {
|
|||
position: relative;
|
||||
z-index: 2;
|
||||
padding: 5px @spacing-standard 0 @spacing-standard;
|
||||
flex-shrink: 0;
|
||||
|
||||
.participant {
|
||||
white-space: nowrap;
|
||||
|
|
|
@ -29,7 +29,7 @@ class MessageToolbarItems extends React.Component
|
|||
componentWillUnmount: =>
|
||||
unsubscribe() for unsubscribe in @_unsubscribers
|
||||
|
||||
_onChange: => _.defer =>
|
||||
_onChange: =>
|
||||
@setState
|
||||
threadIsSelected: FocusedContentStore.focusedId('thread')?
|
||||
|
||||
|
|
|
@ -7,6 +7,8 @@ querystring = require 'querystring'
|
|||
{RetinaImg} = require 'nylas-component-kit'
|
||||
|
||||
class ContainerView extends React.Component
|
||||
@displayName: 'ContainerView'
|
||||
@containerRequired: false
|
||||
|
||||
constructor: (@props) ->
|
||||
@state = @getStateFromStore()
|
||||
|
@ -51,10 +53,12 @@ class ContainerView extends React.Component
|
|||
OnboardingActions.moveToPreviousPage()
|
||||
|
||||
render: =>
|
||||
<ReactCSSTransitionGroup transitionName="page">
|
||||
{@_pageComponent()}
|
||||
<div className="dragRegion" style={"WebkitAppRegion": "drag", position: 'absolute', top:0, left:40, right:0, height: 20, zIndex:100}></div>
|
||||
</ReactCSSTransitionGroup>
|
||||
<div className="onboarding-container">
|
||||
<ReactCSSTransitionGroup transitionName="page">
|
||||
{@_pageComponent()}
|
||||
<div className="dragRegion" style={"WebkitAppRegion": "drag", position: 'absolute', top:0, left:40, right:0, height: 20, zIndex:100}></div>
|
||||
</ReactCSSTransitionGroup>
|
||||
</div>
|
||||
|
||||
_pageComponent: =>
|
||||
if @state.error
|
||||
|
|
|
@ -1,16 +1,11 @@
|
|||
React = require "react"
|
||||
ContainerView = require "./container-view"
|
||||
remote = require "remote"
|
||||
ContainerView = require './container-view'
|
||||
{WorkspaceStore, ComponentRegistry} = require 'nylas-exports'
|
||||
|
||||
module.exports =
|
||||
item: null
|
||||
|
||||
activate: (@state) ->
|
||||
# This package does nothing in other windows
|
||||
return unless atom.getWindowType() is 'onboarding'
|
||||
|
||||
@item = document.createElement("div")
|
||||
@item.setAttribute("id", "onboarding-container")
|
||||
@item.setAttribute("class", "onboarding-container")
|
||||
React.render(<ContainerView /> , @item)
|
||||
document.body.appendChild(@item)
|
||||
WorkspaceStore.defineSheet 'Main', {root: true},
|
||||
list: ['Center']
|
||||
ComponentRegistry.register ContainerView,
|
||||
location: WorkspaceStore.Location.Center
|
||||
|
|
|
@ -59,6 +59,7 @@ class ThreadListParticipants extends React.Component
|
|||
list = []
|
||||
last = null
|
||||
for msg in @props.thread.metadata
|
||||
continue if msg.draft
|
||||
from = msg.from[0]
|
||||
if from and from.email isnt last
|
||||
list.push({
|
||||
|
@ -101,5 +102,4 @@ class ThreadListParticipants extends React.Component
|
|||
list
|
||||
|
||||
|
||||
|
||||
module.exports = ThreadListParticipants
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
_ = require 'underscore'
|
||||
React = require 'react'
|
||||
classNames = require 'classnames'
|
||||
{ListTabular, MultiselectList} = require 'nylas-component-kit'
|
||||
{ListTabular, MultiselectList, RetinaImg} = require 'nylas-component-kit'
|
||||
{timestamp, subject} = require './formatting-utils'
|
||||
{Actions,
|
||||
Utils,
|
||||
|
@ -29,7 +29,7 @@ class ThreadList extends React.Component
|
|||
msgs = thread.metadata
|
||||
return 'unknown' unless msgs and msgs instanceof Array
|
||||
|
||||
msgs = _.filter msgs, (m) -> m.isSaved()
|
||||
msgs = _.filter msgs, (m) -> m.isSaved() and not m.draft
|
||||
msg = msgs[msgs.length - 1]
|
||||
return 'unknown' unless msgs.length > 0
|
||||
|
||||
|
@ -51,7 +51,14 @@ class ThreadList extends React.Component
|
|||
name: "Name"
|
||||
width: 200
|
||||
resolver: (thread) =>
|
||||
<ThreadListParticipants thread={thread} />
|
||||
hasDraft = _.find (thread.metadata ? []), (m) -> m.draft
|
||||
if hasDraft
|
||||
<div style={display: 'flex'}>
|
||||
<ThreadListParticipants thread={thread} />
|
||||
<RetinaImg name="icon-draft-pencil.png" className="draft-icon" />
|
||||
</div>
|
||||
else
|
||||
<ThreadListParticipants thread={thread} />
|
||||
|
||||
c3 = new ListTabular.Column
|
||||
name: "Message"
|
||||
|
|
|
@ -41,6 +41,13 @@ describe "ThreadListParticipants", ->
|
|||
new Message(unread: false, from: [@ben]),
|
||||
]
|
||||
out: [{contact: @ben, unread: false}]
|
||||
},{
|
||||
name: 'single read email and draft'
|
||||
in: [
|
||||
new Message(unread: false, from: [@ben]),
|
||||
new Message(from: [@ben], draft: true),
|
||||
]
|
||||
out: [{contact: @ben, unread: false}]
|
||||
},{
|
||||
name: 'single unread email'
|
||||
in: [
|
||||
|
|
|
@ -26,6 +26,13 @@
|
|||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.draft-icon {
|
||||
margin-top:8px;
|
||||
margin-left:10px;
|
||||
flex-shrink: 0;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.participants {
|
||||
font-size: @font-size-small;
|
||||
text-overflow: ellipsis;
|
||||
|
@ -113,8 +120,8 @@
|
|||
color: @text-color-inverse-subtle;
|
||||
}
|
||||
|
||||
.thread-icon {
|
||||
-webkit-filter: brightness(600%);
|
||||
.thread-icon, .draft-icon {
|
||||
-webkit-filter: brightness(600%) grayscale(100%);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
_ = require 'underscore'
|
||||
{Actions, DatabaseStore, Thread, Tag} = require 'nylas-exports'
|
||||
{Actions, DatabaseStore, NamespaceStore, Thread, Tag} = require 'nylas-exports'
|
||||
|
||||
module.exports =
|
||||
activate: ->
|
||||
|
@ -17,9 +17,13 @@ module.exports =
|
|||
incomingMessages = incoming['message'] ? []
|
||||
incomingThreads = incoming['thread'] ? []
|
||||
|
||||
# Filter for new messages
|
||||
# Filter for new messages that are not sent by the current user
|
||||
myEmail = NamespaceStore.current().emailAddress
|
||||
newUnread = _.filter incomingMessages, (msg) =>
|
||||
msg.unread is true and msg.date?.valueOf() >= @activationTime
|
||||
isUnread = msg.unread is true
|
||||
isNew = msg.date?.valueOf() >= @activationTime
|
||||
isFromMe = msg.from[0]?.email is myEmail
|
||||
return isUnread and isNew and not isFromMe
|
||||
|
||||
return resolve() if newUnread.length is 0
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@ Message = require '../../../src/flux/models/message'
|
|||
Thread = require '../../../src/flux/models/thread'
|
||||
Tag = require '../../../src/flux/models/tag'
|
||||
DatabaseStore = require '../../../src/flux/stores/database-store'
|
||||
NamespaceStore = require '../../../src/flux/stores/namespace-store'
|
||||
Main = require '../lib/main'
|
||||
|
||||
describe "UnreadNotifications", ->
|
||||
|
@ -51,6 +52,12 @@ describe "UnreadNotifications", ->
|
|||
from: [new Contact(name: 'Mark', email: 'mark@example.com')]
|
||||
subject: "Hello World Old"
|
||||
threadId: "A"
|
||||
@msgFromMe = new Message
|
||||
unread: true
|
||||
date: new Date()
|
||||
from: [NamespaceStore.current().me()]
|
||||
subject: "A Sent Mail!"
|
||||
threadId: "A"
|
||||
|
||||
spyOn(DatabaseStore, 'find').andCallFake (klass, id) =>
|
||||
return Promise.resolve(@threadA) if id is 'A'
|
||||
|
@ -121,3 +128,9 @@ describe "UnreadNotifications", ->
|
|||
.then ->
|
||||
expect(window.Notification).not.toHaveBeenCalled()
|
||||
|
||||
it "should not create a Notification if the new message is one I sent", ->
|
||||
waitsForPromise =>
|
||||
Main._onNewMailReceived({message: [@msgFromMe]})
|
||||
.then ->
|
||||
expect(window.Notification).not.toHaveBeenCalled()
|
||||
|
||||
|
|
|
@ -5,6 +5,10 @@ class TestComponent extends React.Component
|
|||
@displayName: 'TestComponent'
|
||||
constructor: ->
|
||||
|
||||
class TestComponentNotSameIdentity extends React.Component
|
||||
@displayName: 'TestComponent'
|
||||
constructor: ->
|
||||
|
||||
class TestComponentNoDisplayName extends React.Component
|
||||
constructor: ->
|
||||
|
||||
|
@ -38,9 +42,13 @@ describe 'ComponentRegistry', ->
|
|||
it 'returns itself', ->
|
||||
expect(ComponentRegistry.register(TestComponent, {role: "bla"})).toBe(ComponentRegistry)
|
||||
|
||||
it 'does not allow components to be overridden by others with the same displayName', ->
|
||||
it 'does allow the exact same component to be redefined with different role/locations', ->
|
||||
ComponentRegistry.register(TestComponent, {role: "bla"})
|
||||
expect(-> ComponentRegistry.register(TestComponent, {role: "bla"})).toThrow()
|
||||
expect(-> ComponentRegistry.register(TestComponent, {role: "other-role"})).not.toThrow()
|
||||
|
||||
it 'does not allow components to be overridden by other components with the same displayName', ->
|
||||
ComponentRegistry.register(TestComponent, {role: "bla"})
|
||||
expect(-> ComponentRegistry.register(TestComponentNotSameIdentity, {role: "bla"})).toThrow()
|
||||
|
||||
it 'does not allow components to be registered without a displayName', ->
|
||||
expect(-> ComponentRegistry.register(TestComponentNoDisplayName, {role: "bla"})).toThrow()
|
||||
|
|
|
@ -445,18 +445,34 @@ class Atom extends Model
|
|||
reload: ->
|
||||
ipc.send('call-window-method', 'restart')
|
||||
|
||||
# Calls the `windowPropsReceived` method of all packages that are
|
||||
# currently loaded
|
||||
# Updates the window load settings - called when the app is ready to display
|
||||
# a hot-loaded window. Causes listeners registered with `onWindowPropsReceived`
|
||||
# to receive new window props.
|
||||
loadSettingsChanged: (loadSettings) =>
|
||||
@loadSettings = loadSettings
|
||||
@constructor.loadSettings = loadSettings
|
||||
{width, height, windowProps} = loadSettings
|
||||
|
||||
@packages.windowPropsReceived(windowProps ? {})
|
||||
@emitter.emit('window-props-received', windowProps ? {})
|
||||
|
||||
if width and height
|
||||
@setWindowDimensions({width, height})
|
||||
|
||||
# Public: The windowProps passed when creating the window via `newWindow`.
|
||||
#
|
||||
getWindowProps: ->
|
||||
@getLoadSettings().windowProps ? {}
|
||||
|
||||
# Public: If your package declares hot-loaded window types, `onWindowPropsReceived`
|
||||
# fires when your hot-loaded window is about to be shown so you can update
|
||||
# components to reflect the new window props.
|
||||
#
|
||||
# - callback: A function to call when window props are received, just before
|
||||
# the hot window is shown. The first parameter is the new windowProps.
|
||||
#
|
||||
onWindowPropsReceived: (callback) ->
|
||||
@emitter.on('window-props-received', callback)
|
||||
|
||||
# Extended: Returns a {Boolean} true when the current window is maximized.
|
||||
isMaximixed: ->
|
||||
@getCurrentWindow().isMaximized()
|
||||
|
@ -579,7 +595,7 @@ class Atom extends Model
|
|||
@keymaps.loadBundledKeymaps()
|
||||
@themes.loadBaseStylesheets()
|
||||
@packages.loadPackages()
|
||||
@deserializeEditorWindow()
|
||||
@deserializeRootWindow()
|
||||
@packages.activate()
|
||||
@keymaps.loadUserKeymap()
|
||||
@requireUserInitScript() unless safeMode
|
||||
|
@ -589,10 +605,10 @@ class Atom extends Model
|
|||
'atom-workspace:add-account': =>
|
||||
options =
|
||||
title: 'Add an Account'
|
||||
frame: false
|
||||
page: 'add-account'
|
||||
width: 340
|
||||
height: 550
|
||||
toolbar: false
|
||||
resizable: false
|
||||
windowType: 'onboarding'
|
||||
windowPackages: ['onboarding']
|
||||
|
@ -611,7 +627,6 @@ class Atom extends Model
|
|||
{width,
|
||||
height,
|
||||
windowType,
|
||||
windowProps,
|
||||
windowPackages} = @getLoadSettings()
|
||||
|
||||
@loadConfig()
|
||||
|
@ -622,10 +637,10 @@ class Atom extends Model
|
|||
|
||||
@packages.loadPackages(windowType)
|
||||
@packages.loadPackage(pack) for pack in (windowPackages ? [])
|
||||
@deserializeSheetContainer()
|
||||
@packages.activate()
|
||||
|
||||
ipc.on("load-settings-changed", @loadSettingsChanged)
|
||||
@packages.windowPropsReceived(windowProps ? {})
|
||||
|
||||
@keymaps.loadUserKeymap()
|
||||
|
||||
|
@ -647,6 +662,9 @@ class Atom extends Model
|
|||
# src/browser/application.coffee
|
||||
registerHotWindow: (options={}) -> ipc.send('register-hot-window', options)
|
||||
|
||||
# Unregisters a hot window with the given windowType
|
||||
unregisterHotWindow: (windowType) -> ipc.send('unregister-hot-window', windowType)
|
||||
|
||||
unloadEditorWindow: ->
|
||||
@packages.deactivatePackages()
|
||||
@savedState.packageStates = @packages.packageStates
|
||||
|
@ -737,7 +755,7 @@ class Atom extends Model
|
|||
Section: Private
|
||||
###
|
||||
|
||||
deserializeWorkspaceView: ->
|
||||
deserializeSheetContainer: ->
|
||||
startTime = Date.now()
|
||||
# Put state back into sheet-container? Restore app state here
|
||||
@deserializeTimings.workspace = Date.now() - startTime
|
||||
|
@ -756,9 +774,9 @@ class Atom extends Model
|
|||
@packages.packageStates = @savedState.packageStates ? {}
|
||||
delete @savedState.packageStates
|
||||
|
||||
deserializeEditorWindow: ->
|
||||
deserializeRootWindow: ->
|
||||
@deserializePackageStates()
|
||||
@deserializeWorkspaceView()
|
||||
@deserializeSheetContainer()
|
||||
|
||||
loadThemes: ->
|
||||
@themes.load()
|
||||
|
|
|
@ -294,6 +294,9 @@ class Application
|
|||
ipc.on 'register-hot-window', (event, options) =>
|
||||
@windowManager.registerHotWindow(options)
|
||||
|
||||
ipc.on 'unregister-hot-window', (event, windowType) =>
|
||||
@windowManager.unregisterHotWindow(windowType)
|
||||
|
||||
app.on 'activate-with-no-open-windows', (event) =>
|
||||
@windowManager.ensurePrimaryWindowOnscreen()
|
||||
event.preventDefault()
|
||||
|
|
|
@ -18,10 +18,10 @@ class AtomWindow
|
|||
isSpec: null
|
||||
|
||||
constructor: (settings={}) ->
|
||||
{frame,
|
||||
title,
|
||||
{title,
|
||||
width,
|
||||
height,
|
||||
toolbar,
|
||||
resizable,
|
||||
pathToOpen,
|
||||
hideMenuBar,
|
||||
|
@ -36,12 +36,23 @@ class AtomWindow
|
|||
# Normalize to make sure drive letter case is consistent on Windows
|
||||
@resourcePath = path.normalize(@resourcePath) if @resourcePath
|
||||
|
||||
# Mac: We'll render a CSS toolbar if `toolbar=true`. No frame required.
|
||||
# Win / Linux: We don't render a toolbar in CSS - include frame if the
|
||||
# window requests a toolbar. Remove this code once we have custom toolbars
|
||||
# on win/linux.
|
||||
|
||||
toolbar ?= true
|
||||
if process.platform is 'darwin'
|
||||
frame = false
|
||||
else
|
||||
frame = toolbar
|
||||
|
||||
options =
|
||||
show: false
|
||||
title: title ? 'Nylas'
|
||||
frame: frame ? true
|
||||
frame: frame
|
||||
#https://atomio.slack.com/archives/electron/p1432056952000608
|
||||
'standard-window': frame ? true
|
||||
'standard-window': frame
|
||||
width: width
|
||||
height: height
|
||||
resizable: resizable ? true
|
||||
|
@ -62,6 +73,7 @@ class AtomWindow
|
|||
@handleEvents()
|
||||
|
||||
loadSettings = _.extend({}, settings)
|
||||
loadSettings.toolbar = toolbar
|
||||
loadSettings.windowState ?= '{}'
|
||||
loadSettings.appVersion = app.getVersion()
|
||||
loadSettings.resourcePath = @resourcePath
|
||||
|
|
|
@ -85,7 +85,6 @@ class WindowManager
|
|||
devMode: @devMode
|
||||
safeMode: @safeMode
|
||||
neverClose: true
|
||||
frame: process.platform isnt 'darwin'
|
||||
mainWindow: true
|
||||
|
||||
|
||||
|
@ -104,7 +103,7 @@ class WindowManager
|
|||
newOnboardingWindow: ->
|
||||
@newWindow
|
||||
title: 'Welcome to Nylas'
|
||||
frame: false
|
||||
toolbar: false
|
||||
width: 340
|
||||
height: 550
|
||||
resizable: false
|
||||
|
@ -136,7 +135,10 @@ class WindowManager
|
|||
#
|
||||
# This means that when `newWindow` is called, instead of going through
|
||||
# the bootup process, it simply replaces key parameters and does a soft
|
||||
# reload via `windowPropsReceived`.
|
||||
# reload.
|
||||
#
|
||||
# To listen for window props being sent to your existing hot-loaded window,
|
||||
# add a callback to `atom.onWindowPropsChanged`.
|
||||
#
|
||||
# Since the window is already loaded, there are only some options that
|
||||
# can be soft-reloaded. If you attempt to pass options that a soft
|
||||
|
@ -157,10 +159,6 @@ class WindowManager
|
|||
# serializable data. No functions!
|
||||
# - title: The title of the page
|
||||
#
|
||||
# Other options that will trigger a
|
||||
# - frame: defaults true. Whether or not the popup has a frame
|
||||
# - forceNewWindow
|
||||
#
|
||||
# Other non required options:
|
||||
# - All of the options of BrowserWindow
|
||||
# https://github.com/atom/electron/blob/master/docs/api/browser-window.md#new-browserwindowoptions
|
||||
|
@ -202,6 +200,28 @@ class WindowManager
|
|||
|
||||
@_replenishHotWindows()
|
||||
|
||||
unregisterHotWindow: (windowType) ->
|
||||
return unless @_hotWindows[windowType]
|
||||
|
||||
# Remove entries from the replentishQueue
|
||||
@_replenishQueue = _.reject @_replenishQueue, (item) => item.windowType is windowType
|
||||
|
||||
# Destroy any hot windows already loaded
|
||||
destroyedLoadingWindow = false
|
||||
{loadedWindows} = @_hotWindows[windowType]
|
||||
for win in loadedWindows
|
||||
destroyedLoadingWindow = true if not win.loaded
|
||||
win.browserWindow.destroy()
|
||||
|
||||
# Delete the hot window configuration
|
||||
delete @_hotWindows[windowType]
|
||||
|
||||
# If we destroyed a window that was currently loading,
|
||||
# the queue will stop processing forever.
|
||||
if destroyedLoadingWindow
|
||||
@_processingQueue = false
|
||||
@_processReplenishQueue()
|
||||
|
||||
# Immediately close all of the hot windows and reset the replentish queue
|
||||
# to prevent more from being opened without additional calls to registerHotWindow.
|
||||
#
|
||||
|
@ -217,6 +237,7 @@ class WindowManager
|
|||
@_hotWindows = {}
|
||||
|
||||
defaultWindowOptions: ->
|
||||
#TODO: Defaults are also applied in AtomWindow.constructor.
|
||||
devMode: @devMode
|
||||
safeMode: @safeMode
|
||||
windowType: 'popout'
|
||||
|
|
|
@ -61,8 +61,8 @@ class ComponentRegistry
|
|||
|
||||
throw new Error("ComponentRegistry.register() requires `role` or `location`") if not roles and not locations
|
||||
|
||||
if @_registry[component.displayName]
|
||||
throw new Error("ComponentRegistry.register(): A component has already been registered with the name #{component.displayName}")
|
||||
if @_registry[component.displayName] and @_registry[component.displayName].component isnt component
|
||||
throw new Error("ComponentRegistry.register(): A different component was already registered with the name #{component.displayName}")
|
||||
|
||||
@_registry[component.displayName] = {component, locations, modes, roles}
|
||||
|
||||
|
|
|
@ -47,8 +47,9 @@ class EventedIFrame extends React.Component
|
|||
doc.removeEventListener('mousedown', @_onIFrameMouseEvent)
|
||||
doc.removeEventListener('mousemove', @_onIFrameMouseEvent)
|
||||
doc.removeEventListener('mouseup', @_onIFrameMouseEvent)
|
||||
node.contentWindow.removeEventListener('focus', @_onIFrameFocus)
|
||||
node.contentWindow.removeEventListener('blur', @_onIFrameBlur)
|
||||
if node.contentWindow
|
||||
node.contentWindow.removeEventListener('focus', @_onIFrameFocus)
|
||||
node.contentWindow.removeEventListener('blur', @_onIFrameBlur)
|
||||
|
||||
_subscribeToIFrameEvents: =>
|
||||
node = React.findDOMNode(@)
|
||||
|
@ -59,8 +60,9 @@ class EventedIFrame extends React.Component
|
|||
doc.addEventListener("mousedown", @_onIFrameMouseEvent)
|
||||
doc.addEventListener("mousemove", @_onIFrameMouseEvent)
|
||||
doc.addEventListener("mouseup", @_onIFrameMouseEvent)
|
||||
node.contentWindow.addEventListener("focus", @_onIFrameFocus)
|
||||
node.contentWindow.addEventListener("blur", @_onIFrameBlur)
|
||||
if node.contentWindow
|
||||
node.contentWindow.addEventListener("focus", @_onIFrameFocus)
|
||||
node.contentWindow.addEventListener("blur", @_onIFrameBlur)
|
||||
|
||||
_onIFrameBlur: (event) =>
|
||||
node = React.findDOMNode(@)
|
||||
|
|
|
@ -312,7 +312,9 @@ class DatabaseView extends ModelView
|
|||
@log("Metadata version #{touchTime} fetched, but out of date (current is #{page.lastTouchTime})")
|
||||
return
|
||||
|
||||
for item in items
|
||||
for item, idx in items
|
||||
if Object.isFrozen(item)
|
||||
item = items[idx] = new @klass(item)
|
||||
item.metadata = results[item.id]
|
||||
page.metadata[item.id] = results[item.id]
|
||||
|
||||
|
|
|
@ -310,12 +310,15 @@ class DraftStore
|
|||
_onPopoutDraftLocalId: (draftLocalId, options = {}) =>
|
||||
return unless NamespaceStore.current()
|
||||
|
||||
options.draftLocalId = draftLocalId
|
||||
save = Promise.resolve()
|
||||
if @_draftSessions[draftLocalId]
|
||||
save = @_draftSessions[draftLocalId].changes.commit()
|
||||
|
||||
atom.newWindow
|
||||
title: "Message"
|
||||
windowType: "composer"
|
||||
windowProps: options
|
||||
save.then =>
|
||||
atom.newWindow
|
||||
title: "Message"
|
||||
windowType: "composer"
|
||||
windowProps: _.extend(options, {draftLocalId})
|
||||
|
||||
_onHandleMailtoLink: (urlString) =>
|
||||
namespace = NamespaceStore.current()
|
||||
|
|
|
@ -24,21 +24,6 @@ class WorkspaceStore
|
|||
@include Listener
|
||||
|
||||
constructor: ->
|
||||
@Location = Location
|
||||
@Sheet = Sheet
|
||||
|
||||
@defineSheet 'Global'
|
||||
|
||||
@defineSheet 'Threads', {root: true},
|
||||
list: ['RootSidebar', 'ThreadList']
|
||||
split: ['RootSidebar', 'ThreadList', 'MessageList', 'MessageListSidebar']
|
||||
|
||||
@defineSheet 'Drafts', {root: true, name: 'Local Drafts'},
|
||||
list: ['RootSidebar', 'DraftList']
|
||||
|
||||
@defineSheet 'Thread', {},
|
||||
list: ['MessageList', 'MessageListSidebar']
|
||||
|
||||
@_resetInstanceVars()
|
||||
|
||||
@listenTo Actions.selectRootSheet, @_onSelectRootSheet
|
||||
|
@ -52,10 +37,24 @@ class WorkspaceStore
|
|||
'application:pop-sheet': => @popSheet()
|
||||
|
||||
_resetInstanceVars: =>
|
||||
@Location = Location = {}
|
||||
@Sheet = Sheet = {}
|
||||
|
||||
@_preferredLayoutMode = 'list'
|
||||
@_sheetStack = []
|
||||
|
||||
@_onSelectRootSheet(Sheet.Threads)
|
||||
if atom.isMainWindow()
|
||||
@defineSheet 'Global'
|
||||
@defineSheet 'Threads', {root: true},
|
||||
list: ['RootSidebar', 'ThreadList']
|
||||
split: ['RootSidebar', 'ThreadList', 'MessageList', 'MessageListSidebar']
|
||||
@defineSheet 'Drafts', {root: true, name: 'Local Drafts'},
|
||||
list: ['RootSidebar', 'DraftList']
|
||||
@defineSheet 'Thread', {},
|
||||
list: ['MessageList', 'MessageListSidebar']
|
||||
@_onSelectRootSheet(Sheet.Threads)
|
||||
else
|
||||
@defineSheet 'Global'
|
||||
|
||||
###
|
||||
Inbound Events
|
||||
|
@ -97,10 +96,13 @@ class WorkspaceStore
|
|||
# Returns a {String}: The current layout mode. Either `split` or `list`
|
||||
#
|
||||
layoutMode: =>
|
||||
if @_preferredLayoutMode in @rootSheet().supportedModes
|
||||
root = @rootSheet()
|
||||
if not root
|
||||
'list'
|
||||
else if @_preferredLayoutMode in root.supportedModes
|
||||
@_preferredLayoutMode
|
||||
else
|
||||
@rootSheet().supportedModes[0]
|
||||
root.supportedModes[0]
|
||||
|
||||
# Returns The top {Sheet} in the current stack. Use this method to determine
|
||||
# the sheet the user is looking at.
|
||||
|
@ -151,6 +153,9 @@ class WorkspaceStore
|
|||
Header: {id: "Sheet:#{id}:Header"}
|
||||
Footer: {id: "Sheet:#{id}:Footer"}
|
||||
|
||||
if options.root and not @rootSheet()
|
||||
@_onSelectRootSheet(Sheet[id])
|
||||
|
||||
@triggerDebounced()
|
||||
|
||||
undefineSheet: (id) =>
|
||||
|
@ -176,7 +181,7 @@ class WorkspaceStore
|
|||
@_sheetStack.pop()
|
||||
@trigger()
|
||||
|
||||
if sheet is Sheet.Thread
|
||||
if Sheet.Thread and sheet is Sheet.Thread
|
||||
Actions.focusInCollection(collection: 'thread', item: null)
|
||||
|
||||
# Return to the root sheet. This method triggers, allowing observers
|
||||
|
|
|
@ -449,6 +449,3 @@ class PackageManager
|
|||
pack.deactivate()
|
||||
delete @activePackages[pack.name]
|
||||
@emitter.emit 'did-deactivate-package', pack
|
||||
|
||||
windowPropsReceived: (windowProps) ->
|
||||
pack.windowPropsReceived?(windowProps) for pack in @getLoadedPackages()
|
||||
|
|
|
@ -154,9 +154,6 @@ class Package
|
|||
|
||||
Q.all([@grammarsPromise, @settingsPromise, @activationDeferred.promise])
|
||||
|
||||
windowPropsReceived: (windowProps) ->
|
||||
@mainModule?.windowPropsReceived?(windowProps)
|
||||
|
||||
activateNow: ->
|
||||
try
|
||||
@activateConfig()
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
React = require 'react/addons'
|
||||
Sheet = require './sheet'
|
||||
Toolbar = require './sheet-toolbar'
|
||||
Flexbox = require './components/flexbox'
|
||||
RetinaImg = require './components/retina-img'
|
||||
InjectedComponentSet = require './components/injected-component-set'
|
||||
|
@ -10,154 +11,6 @@ _ = require 'underscore'
|
|||
ComponentRegistry,
|
||||
WorkspaceStore} = require "nylas-exports"
|
||||
|
||||
class ToolbarSpacer extends React.Component
|
||||
@displayName = 'ToolbarSpacer'
|
||||
@propTypes =
|
||||
order: React.PropTypes.number
|
||||
|
||||
render: =>
|
||||
<div className="item-spacer" style={flex: 1, order:@props.order ? 0}></div>
|
||||
|
||||
class ToolbarBack extends React.Component
|
||||
@displayName = 'ToolbarBack'
|
||||
render: =>
|
||||
<div className="item-back" onClick={@_onClick}>
|
||||
<RetinaImg name="sheet-back.png" />
|
||||
</div>
|
||||
|
||||
_onClick: =>
|
||||
Actions.popSheet()
|
||||
|
||||
class ToolbarWindowControls extends React.Component
|
||||
@displayName = 'ToolbarWindowControls'
|
||||
render: =>
|
||||
<div name="ToolbarWindowControls" className="toolbar-window-controls">
|
||||
<button className="close" onClick={ -> atom.close()}></button>
|
||||
<button className="minimize" onClick={ -> atom.minimize()}></button>
|
||||
<button className="maximize" onClick={ -> atom.maximize()}></button>
|
||||
</div>
|
||||
|
||||
ComponentRegistry.register ToolbarWindowControls,
|
||||
location: WorkspaceStore.Sheet.Global.Toolbar.Left
|
||||
|
||||
class Toolbar extends React.Component
|
||||
displayName = 'Toolbar'
|
||||
|
||||
propTypes =
|
||||
data: React.PropTypes.object
|
||||
depth: React.PropTypes.number
|
||||
|
||||
constructor: (@props) ->
|
||||
@state = @_getStateFromStores()
|
||||
|
||||
componentDidMount: =>
|
||||
@unlisteners = []
|
||||
@unlisteners.push WorkspaceStore.listen (event) =>
|
||||
@setState(@_getStateFromStores())
|
||||
@unlisteners.push ComponentRegistry.listen (event) =>
|
||||
@setState(@_getStateFromStores())
|
||||
window.addEventListener("resize", @_onWindowResize)
|
||||
window.requestAnimationFrame => @recomputeLayout()
|
||||
|
||||
componentWillUnmount: =>
|
||||
window.removeEventListener("resize", @_onWindowResize)
|
||||
unlistener() for unlistener in @unlisteners
|
||||
|
||||
componentWillReceiveProps: (props) =>
|
||||
@setState(@_getStateFromStores(props))
|
||||
|
||||
componentDidUpdate: =>
|
||||
# Wait for other components that are dirty (the actual columns in the sheet)
|
||||
# to update as well.
|
||||
window.requestAnimationFrame => @recomputeLayout()
|
||||
|
||||
shouldComponentUpdate: (nextProps, nextState) =>
|
||||
# This is very important. Because toolbar uses ReactCSSTransitionGroup,
|
||||
# repetitive unnecessary updates can break animations and cause performance issues.
|
||||
not _.isEqual(nextProps, @props) or not _.isEqual(nextState, @state)
|
||||
|
||||
render: =>
|
||||
style =
|
||||
position:'absolute'
|
||||
width:'100%'
|
||||
height:'100%'
|
||||
zIndex: 1
|
||||
|
||||
toolbars = @state.columns.map (components, idx) =>
|
||||
<div style={position: 'absolute', top:0, display:'none'}
|
||||
data-column={idx}
|
||||
key={idx}>
|
||||
{@_flexboxForComponents(components)}
|
||||
</div>
|
||||
|
||||
<div style={style} className={"sheet-toolbar-container mode-#{@state.mode}"}>
|
||||
{toolbars}
|
||||
</div>
|
||||
|
||||
_flexboxForComponents: (components) =>
|
||||
elements = components.map (component) =>
|
||||
<component key={component.displayName} {...@props} />
|
||||
|
||||
<TimeoutTransitionGroup
|
||||
className="item-container"
|
||||
component={Flexbox}
|
||||
direction="row"
|
||||
leaveTimeout={125}
|
||||
enterTimeout={125}
|
||||
transitionName="sheet-toolbar">
|
||||
{elements}
|
||||
<ToolbarSpacer key="spacer-50" order={-50}/>
|
||||
<ToolbarSpacer key="spacer+50" order={50}/>
|
||||
</TimeoutTransitionGroup>
|
||||
|
||||
recomputeLayout: =>
|
||||
# Find our item containers that are tied to specific columns
|
||||
columnToolbarEls = React.findDOMNode(@).querySelectorAll('[data-column]')
|
||||
|
||||
# Find the top sheet in the stack
|
||||
sheet = document.querySelectorAll("[name='Sheet']")[@props.depth]
|
||||
return unless sheet
|
||||
|
||||
# Position item containers so they have the position and width
|
||||
# as their respective columns in the top sheet
|
||||
for columnToolbarEl in columnToolbarEls
|
||||
column = columnToolbarEl.dataset.column
|
||||
columnEl = sheet.querySelector("[data-column='#{column}']")
|
||||
continue unless columnEl
|
||||
|
||||
columnToolbarEl.style.display = 'inherit'
|
||||
columnToolbarEl.style.left = "#{columnEl.offsetLeft}px"
|
||||
columnToolbarEl.style.width = "#{columnEl.offsetWidth}px"
|
||||
|
||||
_onWindowResize: =>
|
||||
@recomputeLayout()
|
||||
|
||||
_getStateFromStores: (props) =>
|
||||
props ?= @props
|
||||
state =
|
||||
mode: WorkspaceStore.layoutMode()
|
||||
columns: []
|
||||
|
||||
# Add items registered to Regions in the current sheet
|
||||
if @props.data?.columns[state.mode]?
|
||||
for loc in @props.data.columns[state.mode]
|
||||
entries = ComponentRegistry.findComponentsMatching({location: loc.Toolbar, mode: state.mode})
|
||||
state.columns.push(entries)
|
||||
|
||||
# Add left items registered to the Sheet instead of to a Region
|
||||
for loc in [WorkspaceStore.Sheet.Global, @props.data]
|
||||
entries = ComponentRegistry.findComponentsMatching({location: loc.Toolbar.Left, mode: state.mode})
|
||||
state.columns[0]?.push(entries...)
|
||||
state.columns[0]?.push(ToolbarBack) if @props.depth > 0
|
||||
|
||||
# Add right items registered to the Sheet instead of to a Region
|
||||
for loc in [WorkspaceStore.Sheet.Global, @props.data]
|
||||
entries = ComponentRegistry.findComponentsMatching({location: loc.Toolbar.Right, mode: state.mode})
|
||||
state.columns[state.columns.length - 1]?.push(entries...)
|
||||
|
||||
state
|
||||
|
||||
|
||||
class SheetContainer extends React.Component
|
||||
displayName = 'SheetContainer'
|
||||
|
||||
|
@ -177,19 +30,13 @@ class SheetContainer extends React.Component
|
|||
totalSheets = @state.stack.length
|
||||
topSheet = @state.stack[totalSheets - 1]
|
||||
|
||||
toolbarElements = @_toolbarElements()
|
||||
return <div></div> unless topSheet
|
||||
|
||||
sheetElements = @_sheetElements()
|
||||
|
||||
<Flexbox direction="column">
|
||||
<div name="Toolbar" style={order:0, zIndex: 3} className="sheet-toolbar">
|
||||
{toolbarElements[0]}
|
||||
<TimeoutTransitionGroup leaveTimeout={125}
|
||||
enterTimeout={125}
|
||||
transitionName="sheet-toolbar">
|
||||
{toolbarElements[1..-1]}
|
||||
</TimeoutTransitionGroup>
|
||||
</div>
|
||||
|
||||
{@_toolbarContainerElement()}
|
||||
|
||||
<div name="Header" style={order:1, zIndex: 2}>
|
||||
<InjectedComponentSet matching={locations: [topSheet.Header, WorkspaceStore.Sheet.Global.Header]}
|
||||
direction="column"
|
||||
|
@ -212,6 +59,20 @@ class SheetContainer extends React.Component
|
|||
</div>
|
||||
</Flexbox>
|
||||
|
||||
_toolbarContainerElement: =>
|
||||
{toolbar} = atom.getLoadSettings()
|
||||
return [] unless toolbar
|
||||
|
||||
toolbarElements = @_toolbarElements()
|
||||
<div name="Toolbar" style={order:0, zIndex: 3} className="sheet-toolbar">
|
||||
{toolbarElements[0]}
|
||||
<TimeoutTransitionGroup leaveTimeout={125}
|
||||
enterTimeout={125}
|
||||
transitionName="sheet-toolbar">
|
||||
{toolbarElements[1..-1]}
|
||||
</TimeoutTransitionGroup>
|
||||
</div>
|
||||
|
||||
_toolbarElements: =>
|
||||
@state.stack.map (sheet, index) ->
|
||||
<Toolbar data={sheet}
|
||||
|
@ -236,4 +97,4 @@ class SheetContainer extends React.Component
|
|||
stack: WorkspaceStore.sheetStack()
|
||||
|
||||
|
||||
module.exports = SheetContainer
|
||||
module.exports = SheetContainer
|
159
src/sheet-toolbar.cjsx
Normal file
159
src/sheet-toolbar.cjsx
Normal file
|
@ -0,0 +1,159 @@
|
|||
React = require 'react/addons'
|
||||
Sheet = require './sheet'
|
||||
Flexbox = require './components/flexbox'
|
||||
RetinaImg = require './components/retina-img'
|
||||
TimeoutTransitionGroup = require './components/timeout-transition-group'
|
||||
_ = require 'underscore'
|
||||
|
||||
{Actions,
|
||||
ComponentRegistry,
|
||||
WorkspaceStore} = require "nylas-exports"
|
||||
|
||||
class ToolbarSpacer extends React.Component
|
||||
@displayName: 'ToolbarSpacer'
|
||||
@propTypes:
|
||||
order: React.PropTypes.number
|
||||
|
||||
render: =>
|
||||
<div className="item-spacer" style={flex: 1, order:@props.order ? 0}></div>
|
||||
|
||||
class ToolbarBack extends React.Component
|
||||
@displayName: 'ToolbarBack'
|
||||
render: =>
|
||||
<div className="item-back" onClick={@_onClick}>
|
||||
<RetinaImg name="sheet-back.png" />
|
||||
</div>
|
||||
|
||||
_onClick: =>
|
||||
Actions.popSheet()
|
||||
|
||||
class ToolbarWindowControls extends React.Component
|
||||
@displayName: 'ToolbarWindowControls'
|
||||
render: =>
|
||||
<div name="ToolbarWindowControls" className="toolbar-window-controls">
|
||||
<button className="close" onClick={ -> atom.close()}></button>
|
||||
<button className="minimize" onClick={ -> atom.minimize()}></button>
|
||||
<button className="maximize" onClick={ -> atom.maximize()}></button>
|
||||
</div>
|
||||
|
||||
ComponentRegistry.register ToolbarWindowControls,
|
||||
location: WorkspaceStore.Sheet.Global.Toolbar.Left
|
||||
|
||||
class Toolbar extends React.Component
|
||||
@displayName: 'Toolbar'
|
||||
|
||||
@propTypes:
|
||||
data: React.PropTypes.object
|
||||
depth: React.PropTypes.number
|
||||
|
||||
constructor: (@props) ->
|
||||
@state = @_getStateFromStores()
|
||||
|
||||
componentDidMount: =>
|
||||
@unlisteners = []
|
||||
@unlisteners.push WorkspaceStore.listen (event) =>
|
||||
@setState(@_getStateFromStores())
|
||||
@unlisteners.push ComponentRegistry.listen (event) =>
|
||||
@setState(@_getStateFromStores())
|
||||
window.addEventListener("resize", @_onWindowResize)
|
||||
window.requestAnimationFrame => @recomputeLayout()
|
||||
|
||||
componentWillUnmount: =>
|
||||
window.removeEventListener("resize", @_onWindowResize)
|
||||
unlistener() for unlistener in @unlisteners
|
||||
|
||||
componentWillReceiveProps: (props) =>
|
||||
@setState(@_getStateFromStores(props))
|
||||
|
||||
componentDidUpdate: =>
|
||||
# Wait for other components that are dirty (the actual columns in the sheet)
|
||||
# to update as well.
|
||||
window.requestAnimationFrame => @recomputeLayout()
|
||||
|
||||
shouldComponentUpdate: (nextProps, nextState) =>
|
||||
# This is very important. Because toolbar uses ReactCSSTransitionGroup,
|
||||
# repetitive unnecessary updates can break animations and cause performance issues.
|
||||
not _.isEqual(nextProps, @props) or not _.isEqual(nextState, @state)
|
||||
|
||||
render: =>
|
||||
style =
|
||||
position:'absolute'
|
||||
width:'100%'
|
||||
height:'100%'
|
||||
zIndex: 1
|
||||
|
||||
toolbars = @state.columns.map (components, idx) =>
|
||||
<div style={position: 'absolute', top:0, display:'none'}
|
||||
data-column={idx}
|
||||
key={idx}>
|
||||
{@_flexboxForComponents(components)}
|
||||
</div>
|
||||
|
||||
<div style={style} className={"sheet-toolbar-container mode-#{@state.mode}"}>
|
||||
{toolbars}
|
||||
</div>
|
||||
|
||||
_flexboxForComponents: (components) =>
|
||||
elements = components.map (component) =>
|
||||
<component key={component.displayName} {...@props} />
|
||||
|
||||
<TimeoutTransitionGroup
|
||||
className="item-container"
|
||||
component={Flexbox}
|
||||
direction="row"
|
||||
leaveTimeout={125}
|
||||
enterTimeout={125}
|
||||
transitionName="sheet-toolbar">
|
||||
{elements}
|
||||
<ToolbarSpacer key="spacer-50" order={-50}/>
|
||||
<ToolbarSpacer key="spacer+50" order={50}/>
|
||||
</TimeoutTransitionGroup>
|
||||
|
||||
recomputeLayout: =>
|
||||
# Find our item containers that are tied to specific columns
|
||||
columnToolbarEls = React.findDOMNode(@).querySelectorAll('[data-column]')
|
||||
|
||||
# Find the top sheet in the stack
|
||||
sheet = document.querySelectorAll("[name='Sheet']")[@props.depth]
|
||||
return unless sheet
|
||||
|
||||
# Position item containers so they have the position and width
|
||||
# as their respective columns in the top sheet
|
||||
for columnToolbarEl in columnToolbarEls
|
||||
column = columnToolbarEl.dataset.column
|
||||
columnEl = sheet.querySelector("[data-column='#{column}']")
|
||||
continue unless columnEl
|
||||
|
||||
columnToolbarEl.style.display = 'inherit'
|
||||
columnToolbarEl.style.left = "#{columnEl.offsetLeft}px"
|
||||
columnToolbarEl.style.width = "#{columnEl.offsetWidth}px"
|
||||
|
||||
_onWindowResize: =>
|
||||
@recomputeLayout()
|
||||
|
||||
_getStateFromStores: (props) =>
|
||||
props ?= @props
|
||||
state =
|
||||
mode: WorkspaceStore.layoutMode()
|
||||
columns: []
|
||||
|
||||
# Add items registered to Regions in the current sheet
|
||||
if @props.data?.columns[state.mode]?
|
||||
for loc in @props.data.columns[state.mode]
|
||||
entries = ComponentRegistry.findComponentsMatching({location: loc.Toolbar, mode: state.mode})
|
||||
state.columns.push(entries)
|
||||
|
||||
# Add left items registered to the Sheet instead of to a Region
|
||||
for loc in [WorkspaceStore.Sheet.Global, @props.data]
|
||||
entries = ComponentRegistry.findComponentsMatching({location: loc.Toolbar.Left, mode: state.mode})
|
||||
state.columns[0]?.push(entries...)
|
||||
state.columns[0]?.push(ToolbarBack) if @props.depth > 0
|
||||
|
||||
# Add right items registered to the Sheet instead of to a Region
|
||||
for loc in [WorkspaceStore.Sheet.Global, @props.data]
|
||||
entries = ComponentRegistry.findComponentsMatching({location: loc.Toolbar.Right, mode: state.mode})
|
||||
state.columns[state.columns.length - 1]?.push(entries...)
|
||||
|
||||
state
|
||||
|
||||
module.exports = Toolbar
|
BIN
static/images/thread-list/icon-draft-pencil@2x.png
Normal file
BIN
static/images/thread-list/icon-draft-pencil@2x.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 15 KiB |
|
@ -19,8 +19,8 @@ atom-workspace {
|
|||
position: relative;
|
||||
font-family: @font-family;
|
||||
|
||||
/* Important: This attribute is used in the theme-manager-specs to check that
|
||||
themes load and override each other correctly. Do not remove! */
|
||||
// Important: This attribute is used in the theme-manager-specs to check that
|
||||
// themes load and override each other correctly. Do not remove!
|
||||
background-color: @background-primary;
|
||||
|
||||
atom-workspace-axis.horizontal {
|
||||
|
|
Loading…
Reference in a new issue