Merge pull request #1410 from atom/ns-rename-pane-views

Add -View suffix to pane views and remove -Model suffix from pane models
This commit is contained in:
Nathan Sobo 2014-01-13 13:39:13 -08:00
commit 08716fd888
17 changed files with 818 additions and 511 deletions

View File

@ -1,7 +1,7 @@
PaneContainerModel = require '../src/pane-container-model'
PaneModel = require '../src/pane-model'
PaneContainer = require '../src/pane-container'
Pane = require '../src/pane'
describe "PaneContainerModel", ->
describe "PaneContainer", ->
describe "serialization", ->
[containerA, pane1A, pane2A, pane3A] = []
@ -12,8 +12,8 @@ describe "PaneContainerModel", ->
@deserialize: -> new this
serialize: -> deserializer: 'Item'
pane1A = new PaneModel(items: [new Item])
containerA = new PaneContainerModel(root: pane1A)
pane1A = new Pane(items: [new Item])
containerA = new PaneContainer(root: pane1A)
pane2A = pane1A.splitRight(items: [new Item])
pane3A = pane2A.splitDown(items: [new Item])
@ -36,8 +36,8 @@ describe "PaneContainerModel", ->
[container, pane1, pane2] = []
beforeEach ->
pane1 = new PaneModel
container = new PaneContainerModel(root: pane1)
pane1 = new Pane
container = new PaneContainer(root: pane1)
it "references the first pane if no pane has been made active", ->
expect(container.activePane).toBe pane1
@ -67,8 +67,8 @@ describe "PaneContainerModel", ->
[container, pane, surrenderedFocusHandler] = []
beforeEach ->
pane = new PaneModel
container = new PaneContainerModel(root: pane)
pane = new Pane
container = new PaneContainer(root: pane)
container.on 'surrendered-focus', surrenderedFocusHandler = jasmine.createSpy("surrenderedFocusHandler")
it "assigns null to the root and the activePane", ->

View File

@ -1,10 +1,10 @@
path = require 'path'
temp = require 'temp'
PaneContainer = require '../src/pane-container'
Pane = require '../src/pane'
PaneContainerView = require '../src/pane-container-view'
PaneView = require '../src/pane-view'
{_, $, View, $$} = require 'atom'
describe "PaneContainer", ->
describe "PaneContainerView", ->
[TestView, container, pane1, pane2, pane3] = []
beforeEach ->
@ -18,8 +18,8 @@ describe "PaneContainer", ->
save: -> @saved = true
isEqual: (other) -> @name is other?.name
container = new PaneContainer
pane1 = new Pane(new TestView('1'))
container = new PaneContainerView
pane1 = new PaneView(new TestView('1'))
container.setRoot(pane1)
pane2 = pane1.splitRight(new TestView('2'))
pane3 = pane2.splitDown(new TestView('3'))
@ -146,9 +146,9 @@ describe "PaneContainer", ->
item2b = new TestView('2b')
item3a = new TestView('3a')
container = new PaneContainer
container = new PaneContainerView
container.attachToDom()
pane1 = new Pane(item1a)
pane1 = new PaneView(item1a)
container.setRoot(pane1)
activeItemChangedHandler = jasmine.createSpy("activeItemChangedHandler")
@ -160,7 +160,7 @@ describe "PaneContainer", ->
expect(container.getPanes().length).toBe 0
activeItemChangedHandler.reset()
pane = new Pane(item1a)
pane = new PaneView(item1a)
container.setRoot(pane)
expect(activeItemChangedHandler.callCount).toBe 1
expect(activeItemChangedHandler.argsForCall[0][1]).toEqual item1a

View File

@ -1,15 +1,15 @@
{Model} = require 'theorist'
PaneModel = require '../src/pane-model'
PaneAxisModel = require '../src/pane-axis-model'
PaneContainerModel = require '../src/pane-container-model'
Pane = require '../src/pane'
PaneAxis = require '../src/pane-axis'
PaneContainer = require '../src/pane-container'
describe "PaneModel", ->
describe "Pane", ->
describe "split methods", ->
[pane1, container] = []
beforeEach ->
pane1 = new PaneModel(items: ["A"])
container = new PaneContainerModel(root: pane1)
pane1 = new Pane(items: ["A"])
container = new PaneContainer(root: pane1)
describe "::splitLeft(params)", ->
describe "when the parent is the container root", ->
@ -87,14 +87,14 @@ describe "PaneModel", ->
describe "::destroyItem(item)", ->
describe "when the last item is destroyed", ->
it "destroys the pane", ->
pane = new PaneModel(items: ["A", "B"])
pane = new Pane(items: ["A", "B"])
pane.destroyItem("A")
pane.destroyItem("B")
expect(pane.isDestroyed()).toBe true
describe "when an item emits a destroyed event", ->
it "removes it from the list of items", ->
pane = new PaneModel(items: [new Model, new Model, new Model])
pane = new Pane(items: [new Model, new Model, new Model])
[item1, item2, item3] = pane.items
pane.items[1].destroy()
expect(pane.items).toEqual [item1, item3]
@ -103,8 +103,8 @@ describe "PaneModel", ->
[pane1, container] = []
beforeEach ->
pane1 = new PaneModel(items: [new Model, new Model])
container = new PaneContainerModel(root: pane1)
pane1 = new Pane(items: [new Model, new Model])
container = new PaneContainer(root: pane1)
it "destroys the pane's destroyable items", ->
[item1, item2] = pane1.items

View File

@ -1,10 +1,10 @@
PaneContainer = require '../src/pane-container'
Pane = require '../src/pane'
PaneContainerView = require '../src/pane-container-view'
PaneView = require '../src/pane-view'
{fs, $, View} = require 'atom'
path = require 'path'
temp = require 'temp'
describe "Pane", ->
describe "PaneView", ->
[container, view1, view2, editor1, editor2, pane] = []
class TestView extends View
@ -17,12 +17,12 @@ describe "Pane", ->
beforeEach ->
atom.deserializers.add(TestView)
container = new PaneContainer
container = new PaneContainerView
view1 = new TestView(id: 'view-1', text: 'View 1')
view2 = new TestView(id: 'view-2', text: 'View 2')
editor1 = atom.project.openSync('sample.js')
editor2 = atom.project.openSync('sample.txt')
pane = new Pane(view1, editor1, view2, editor2)
pane = new PaneView(view1, editor1, view2, editor2)
container.setRoot(pane)
afterEach ->

View File

@ -2,7 +2,7 @@
Q = require 'q'
path = require 'path'
temp = require 'temp'
Pane = require '../src/pane'
PaneView = require '../src/pane-view'
describe "WorkspaceView", ->
pathToOpen = null
@ -212,7 +212,7 @@ describe "WorkspaceView", ->
describe ".openSync(filePath, options)", ->
describe "when there is no active pane", ->
beforeEach ->
spyOn(Pane.prototype, 'focus')
spyOn(PaneView.prototype, 'focus')
atom.workspaceView.getActivePane().remove()
expect(atom.workspaceView.getActivePane()).toBeUndefined()
@ -360,7 +360,7 @@ describe "WorkspaceView", ->
describe ".open(filePath)", ->
beforeEach ->
spyOn(Pane.prototype, 'focus')
spyOn(PaneView.prototype, 'focus')
describe "when there is no active pane", ->
beforeEach ->

View File

@ -1,66 +0,0 @@
{Model, Sequence} = require 'theorist'
{flatten} = require 'underscore-plus'
Serializable = require 'serializable'
PaneRow = null
PaneColumn = null
module.exports =
class PaneAxisModel extends Model
atom.deserializers.add(this)
Serializable.includeInto(this)
constructor: ({@container, @orientation, children}) ->
@children = Sequence.fromArray(children ? [])
@subscribe @children.onEach (child) =>
child.parent = this
child.container = @container
@subscribe child, 'destroyed', => @removeChild(child)
@subscribe @children.onRemoval (child) => @unsubscribe(child)
@when @children.$length.becomesLessThan(2), 'reparentLastChild'
@when @children.$length.becomesLessThan(1), 'destroy'
deserializeParams: (params) ->
{container} = params
params.children = params.children.map (childState) -> atom.deserializers.deserialize(childState, {container})
params
serializeParams: ->
children: @children.map (child) -> child.serialize()
orientation: @orientation
getViewClass: ->
if @orientation is 'vertical'
PaneColumn ?= require './pane-column'
else
PaneRow ?= require './pane-row'
getPanes: ->
flatten(@children.map (child) -> child.getPanes())
addChild: (child, index=@children.length) ->
@children.splice(index, 0, child)
removeChild: (child) ->
index = @children.indexOf(child)
throw new Error("Removing non-existent child") if index is -1
@children.splice(index, 1)
replaceChild: (oldChild, newChild) ->
index = @children.indexOf(oldChild)
throw new Error("Replacing non-existent child") if index is -1
@children.splice(index, 1, newChild)
insertChildBefore: (currentChild, newChild) ->
index = @children.indexOf(currentChild)
@children.splice(index, 0, newChild)
insertChildAfter: (currentChild, newChild) ->
index = @children.indexOf(currentChild)
@children.splice(index + 1, 0, newChild)
reparentLastChild: ->
@parent.replaceChild(this, @children[0])

34
src/pane-axis-view.coffee Normal file
View File

@ -0,0 +1,34 @@
{View} = require './space-pen-extensions'
PaneView = null
### Internal ###
module.exports =
class PaneAxisView extends View
initialize: (@model) ->
@onChildAdded(child) for child in @model.children
@subscribe @model.children, 'changed', @onChildrenChanged
viewForModel: (model) ->
viewClass = model.getViewClass()
model._view ?= new viewClass(model)
onChildrenChanged: ({index, removedValues, insertedValues}) =>
focusedElement = document.activeElement if @hasFocus()
@onChildRemoved(child, index) for child in removedValues
@onChildAdded(child, index + i) for child, i in insertedValues
focusedElement?.focus() if document.activeElement is document.body
onChildAdded: (child, index) =>
view = @viewForModel(child)
@insertAt(index, view)
onChildRemoved: (child) =>
view = @viewForModel(child)
view.detach()
PaneView ?= require './pane-view'
if view instanceof PaneView and view.model.isDestroyed()
@getContainer()?.trigger 'pane:removed', [view]
getContainer: ->
@closest('.panes').view()

View File

@ -1,34 +1,66 @@
{View} = require './space-pen-extensions'
Pane = null
{Model, Sequence} = require 'theorist'
{flatten} = require 'underscore-plus'
Serializable = require 'serializable'
PaneRowView = null
PaneColumnView = null
### Internal ###
module.exports =
class PaneAxis extends View
initialize: (@model) ->
@onChildAdded(child) for child in @model.children
@subscribe @model.children, 'changed', @onChildrenChanged
class PaneAxis extends Model
atom.deserializers.add(this)
Serializable.includeInto(this)
viewForModel: (model) ->
viewClass = model.getViewClass()
model._view ?= new viewClass(model)
constructor: ({@container, @orientation, children}) ->
@children = Sequence.fromArray(children ? [])
onChildrenChanged: ({index, removedValues, insertedValues}) =>
focusedElement = document.activeElement if @hasFocus()
@onChildRemoved(child, index) for child in removedValues
@onChildAdded(child, index + i) for child, i in insertedValues
focusedElement?.focus() if document.activeElement is document.body
@subscribe @children.onEach (child) =>
child.parent = this
child.container = @container
@subscribe child, 'destroyed', => @removeChild(child)
onChildAdded: (child, index) =>
view = @viewForModel(child)
@insertAt(index, view)
@subscribe @children.onRemoval (child) => @unsubscribe(child)
onChildRemoved: (child) =>
view = @viewForModel(child)
view.detach()
Pane ?= require './pane'
@when @children.$length.becomesLessThan(2), 'reparentLastChild'
@when @children.$length.becomesLessThan(1), 'destroy'
if view instanceof Pane and view.model.isDestroyed()
@getContainer()?.trigger 'pane:removed', [view]
deserializeParams: (params) ->
{container} = params
params.children = params.children.map (childState) -> atom.deserializers.deserialize(childState, {container})
params
getContainer: ->
@closest('.panes').view()
serializeParams: ->
children: @children.map (child) -> child.serialize()
orientation: @orientation
getViewClass: ->
if @orientation is 'vertical'
PaneColumnView ?= require './pane-column-view'
else
PaneRowView ?= require './pane-row-view'
getPanes: ->
flatten(@children.map (child) -> child.getPanes())
addChild: (child, index=@children.length) ->
@children.splice(index, 0, child)
removeChild: (child) ->
index = @children.indexOf(child)
throw new Error("Removing non-existent child") if index is -1
@children.splice(index, 1)
replaceChild: (oldChild, newChild) ->
index = @children.indexOf(oldChild)
throw new Error("Replacing non-existent child") if index is -1
@children.splice(index, 1, newChild)
insertChildBefore: (currentChild, newChild) ->
index = @children.indexOf(currentChild)
@children.splice(index, 0, newChild)
insertChildAfter: (currentChild, newChild) ->
index = @children.indexOf(currentChild)
@children.splice(index + 1, 0, newChild)
reparentLastChild: ->
@parent.replaceChild(this, @children[0])

View File

@ -1,10 +1,10 @@
{$} = require './space-pen-extensions'
_ = require 'underscore-plus'
PaneAxis = require './pane-axis'
PaneAxisView = require './pane-axis-view'
# Internal:
module.exports =
class PaneColumn extends PaneAxis
class PaneColumnView extends PaneAxisView
@content: ->
@div class: 'pane-column'

View File

@ -1,66 +0,0 @@
{Model} = require 'theorist'
Serializable = require 'serializable'
PaneModel = require './pane-model'
module.exports =
class PaneContainerModel extends Model
atom.deserializers.add(this)
Serializable.includeInto(this)
@properties
root: null
activePane: null
previousRoot: null
@behavior 'activePaneItem', ->
@$activePane.switch (activePane) -> activePane?.$activeItem
constructor: (params) ->
super
@subscribe @$root, @onRootChanged
@destroyEmptyPanes() if params?.destroyEmptyPanes
deserializeParams: (params) ->
params.root = atom.deserializers.deserialize(params.root, container: this)
params.destroyEmptyPanes = true
params
serializeParams: (params) ->
root: @root?.serialize()
replaceChild: (oldChild, newChild) ->
throw new Error("Replacing non-existent child") if oldChild isnt @root
@root = newChild
getPanes: ->
@root?.getPanes() ? []
activateNextPane: ->
panes = @getPanes()
if panes.length > 1
currentIndex = panes.indexOf(@activePane)
nextIndex = (currentIndex + 1) % panes.length
panes[nextIndex].activate()
else
@activePane = null
onRootChanged: (root) =>
@unsubscribe(@previousRoot) if @previousRoot?
@previousRoot = root
unless root?
@activePane = null
return
root.parent = this
root.container = this
if root instanceof PaneModel
@activePane ?= root
@subscribe root, 'destroyed', =>
@activePane = null
@root = null
destroyEmptyPanes: ->
pane.destroy() for pane in @getPanes() when pane.items.length is 0

View File

@ -0,0 +1,132 @@
Serializable = require 'serializable'
{$, View} = require './space-pen-extensions'
PaneView = require './pane-view'
PaneContainer = require './pane-container'
# Private: Manages the list of panes within a {WorkspaceView}
module.exports =
class PaneContainerView extends View
atom.deserializers.add(this)
Serializable.includeInto(this)
@deserialize: (state) ->
new this(PaneContainer.deserialize(state.model))
@content: ->
@div class: 'panes'
initialize: (params) ->
if params instanceof PaneContainer
@model = params
else
@model = new PaneContainer({root: params?.root?.model})
@subscribe @model.$root, @onRootChanged
@subscribe @model.$activePaneItem.changes, @onActivePaneItemChanged
viewForModel: (model) ->
if model?
viewClass = model.getViewClass()
model._view ?= new viewClass(model)
serializeParams: ->
model: @model.serialize()
### Public ###
itemDestroyed: (item) ->
@trigger 'item-destroyed', [item]
getRoot: ->
@children().first().view()
setRoot: (root) ->
@model.root = root?.model
onRootChanged: (root) =>
focusedElement = document.activeElement if @hasFocus()
oldRoot = @getRoot()
if oldRoot instanceof PaneView and oldRoot.model.isDestroyed()
@trigger 'pane:removed', [oldRoot]
oldRoot?.detach()
if root?
view = @viewForModel(root)
@append(view)
focusedElement?.focus()
else
atom.workspaceView?.focus() if focusedElement?
onActivePaneItemChanged: (activeItem) =>
@trigger 'pane-container:active-pane-item-changed', [activeItem]
removeChild: (child) ->
throw new Error("Removing non-existant child") unless @getRoot() is child
@setRoot(null)
@trigger 'pane:removed', [child] if child instanceof PaneView
saveAll: ->
pane.saveItems() for pane in @getPanes()
confirmClose: ->
saved = true
for pane in @getPanes()
for item in pane.getItems()
if not pane.promptToSaveItem(item)
saved = false
break
saved
getPanes: ->
@find('.pane').views()
indexOfPane: (pane) ->
@getPanes().indexOf(pane.view())
paneAtIndex: (index) ->
@getPanes()[index]
eachPane: (callback) ->
callback(pane) for pane in @getPanes()
paneAttached = (e) -> callback($(e.target).view())
@on 'pane:attached', paneAttached
off: => @off 'pane:attached', paneAttached
getFocusedPane: ->
@find('.pane:has(:focus)').view()
getActivePane: ->
@viewForModel(@model.activePane)
getActivePaneItem: ->
@model.activePaneItem
getActiveView: ->
@getActivePane()?.activeView
paneForUri: (uri) ->
for pane in @getPanes()
view = pane.itemForUri(uri)
return pane if view?
null
focusNextPane: ->
panes = @getPanes()
if panes.length > 1
currentIndex = panes.indexOf(@getFocusedPane())
nextIndex = (currentIndex + 1) % panes.length
panes[nextIndex].focus()
true
else
false
focusPreviousPane: ->
panes = @getPanes()
if panes.length > 1
currentIndex = panes.indexOf(@getFocusedPane())
previousIndex = currentIndex - 1
previousIndex = panes.length - 1 if previousIndex < 0
panes[previousIndex].focus()
true
else
false

View File

@ -1,132 +1,66 @@
{Model} = require 'theorist'
Serializable = require 'serializable'
{$, View} = require './space-pen-extensions'
Pane = require './pane'
PaneContainerModel = require './pane-container-model'
# Private: Manages the list of panes within a {WorkspaceView}
module.exports =
class PaneContainer extends View
class PaneContainer extends Model
atom.deserializers.add(this)
Serializable.includeInto(this)
@deserialize: (state) ->
new this(PaneContainerModel.deserialize(state.model))
@properties
root: null
activePane: null
@content: ->
@div class: 'panes'
previousRoot: null
initialize: (params) ->
if params instanceof PaneContainerModel
@model = params
else
@model = new PaneContainerModel({root: params?.root?.model})
@behavior 'activePaneItem', ->
@$activePane.switch (activePane) -> activePane?.$activeItem
@subscribe @model.$root, @onRootChanged
@subscribe @model.$activePaneItem.changes, @onActivePaneItemChanged
constructor: (params) ->
super
@subscribe @$root, @onRootChanged
@destroyEmptyPanes() if params?.destroyEmptyPanes
viewForModel: (model) ->
if model?
viewClass = model.getViewClass()
model._view ?= new viewClass(model)
deserializeParams: (params) ->
params.root = atom.deserializers.deserialize(params.root, container: this)
params.destroyEmptyPanes = true
params
serializeParams: ->
model: @model.serialize()
serializeParams: (params) ->
root: @root?.serialize()
### Public ###
itemDestroyed: (item) ->
@trigger 'item-destroyed', [item]
getRoot: ->
@children().first().view()
setRoot: (root) ->
@model.root = root?.model
onRootChanged: (root) =>
focusedElement = document.activeElement if @hasFocus()
oldRoot = @getRoot()
if oldRoot instanceof Pane and oldRoot.model.isDestroyed()
@trigger 'pane:removed', [oldRoot]
oldRoot?.detach()
if root?
view = @viewForModel(root)
@append(view)
focusedElement?.focus()
else
atom.workspaceView?.focus() if focusedElement?
onActivePaneItemChanged: (activeItem) =>
@trigger 'pane-container:active-pane-item-changed', [activeItem]
removeChild: (child) ->
throw new Error("Removing non-existant child") unless @getRoot() is child
@setRoot(null)
@trigger 'pane:removed', [child] if child instanceof Pane
saveAll: ->
pane.saveItems() for pane in @getPanes()
confirmClose: ->
saved = true
for pane in @getPanes()
for item in pane.getItems()
if not pane.promptToSaveItem(item)
saved = false
break
saved
replaceChild: (oldChild, newChild) ->
throw new Error("Replacing non-existent child") if oldChild isnt @root
@root = newChild
getPanes: ->
@find('.pane').views()
@root?.getPanes() ? []
indexOfPane: (pane) ->
@getPanes().indexOf(pane.view())
paneAtIndex: (index) ->
@getPanes()[index]
eachPane: (callback) ->
callback(pane) for pane in @getPanes()
paneAttached = (e) -> callback($(e.target).view())
@on 'pane:attached', paneAttached
off: => @off 'pane:attached', paneAttached
getFocusedPane: ->
@find('.pane:has(:focus)').view()
getActivePane: ->
@viewForModel(@model.activePane)
getActivePaneItem: ->
@model.activePaneItem
getActiveView: ->
@getActivePane()?.activeView
paneForUri: (uri) ->
for pane in @getPanes()
view = pane.itemForUri(uri)
return pane if view?
null
focusNextPane: ->
activateNextPane: ->
panes = @getPanes()
if panes.length > 1
currentIndex = panes.indexOf(@getFocusedPane())
currentIndex = panes.indexOf(@activePane)
nextIndex = (currentIndex + 1) % panes.length
panes[nextIndex].focus()
true
panes[nextIndex].activate()
else
false
@activePane = null
focusPreviousPane: ->
panes = @getPanes()
if panes.length > 1
currentIndex = panes.indexOf(@getFocusedPane())
previousIndex = currentIndex - 1
previousIndex = panes.length - 1 if previousIndex < 0
panes[previousIndex].focus()
true
else
false
onRootChanged: (root) =>
@unsubscribe(@previousRoot) if @previousRoot?
@previousRoot = root
unless root?
@activePane = null
return
root.parent = this
root.container = this
if root instanceof Pane
@activePane ?= root
@subscribe root, 'destroyed', =>
@activePane = null
@root = null
destroyEmptyPanes: ->
pane.destroy() for pane in @getPanes() when pane.items.length is 0

View File

@ -2,8 +2,8 @@
{dirname} = require 'path'
{Model, Sequence} = require 'theorist'
Serializable = require 'serializable'
PaneAxisModel = require './pane-axis-model'
Pane = null
PaneAxis = require './pane-axis'
PaneView = null
# Public: A container for multiple items, one of which is *active* at a given
# time. With the default packages, a tab is displayed for each item and the
@ -58,7 +58,7 @@ class PaneModel extends Model
params
# Private: Called by the view layer to construct a view for this model.
getViewClass: -> Pane ?= require './pane'
getViewClass: -> PaneView ?= require './pane-view'
isActive: -> @active
@ -296,7 +296,7 @@ class PaneModel extends Model
# Private:
split: (orientation, side, params) ->
if @parent.orientation isnt orientation
@parent.replaceChild(this, new PaneAxisModel({@container, orientation, children: [this]}))
@parent.replaceChild(this, new PaneAxis({@container, orientation, children: [this]}))
newPane = new @constructor(extend({focused: true}, params))
switch side

View File

@ -1,11 +1,11 @@
{$} = require './space-pen-extensions'
_ = require 'underscore-plus'
PaneAxis = require './pane-axis'
PaneAxisView = require './pane-axis-view'
### Internal ###
module.exports =
class PaneRow extends PaneAxis
class PaneRowView extends PaneAxisView
@content: ->
@div class: 'pane-row'

226
src/pane-view.coffee Normal file
View File

@ -0,0 +1,226 @@
{$, View} = require './space-pen-extensions'
Serializable = require 'serializable'
Delegator = require 'delegato'
Pane = require './pane'
# Public: A container which can contains multiple items to be switched between.
#
# Items can be almost anything however most commonly they're {EditorView}s.
#
# Most packages won't need to use this class, unless you're interested in
# building a package that deals with switching between panes or tiems.
module.exports =
class PaneView extends View
Serializable.includeInto(this)
Delegator.includeInto(this)
@version: 1
@deserialize: (state) ->
new this(Pane.deserialize(state.model))
@content: (wrappedView) ->
@div class: 'pane', tabindex: -1, =>
@div class: 'item-views', outlet: 'itemViews'
@delegatesProperties 'items', 'activeItem', toProperty: 'model'
@delegatesMethods 'getItems', 'activateNextItem', 'activatePreviousItem', 'getActiveItemIndex',
'activateItemAtIndex', 'activateItem', 'addItem', 'itemAtIndex', 'moveItem', 'moveItemToPane',
'destroyItem', 'destroyItems', 'destroyActiveItem', 'destroyInactiveItems',
'saveActiveItem', 'saveActiveItemAs', 'saveItem', 'saveItemAs', 'saveItems',
'itemForUri', 'activateItemForUri', 'promptToSaveItem', 'copyActiveItem', 'isActive',
'activate', toProperty: 'model'
previousActiveItem: null
# Private:
initialize: (args...) ->
if args[0] instanceof Pane
@model = args[0]
else
@model = new Pane(items: args)
@model._view = this
@onItemAdded(item) for item in @items
@viewsByItem = new WeakMap()
@handleEvents()
handleEvents: ->
@subscribe @model, 'destroyed', => @remove()
@subscribe @model.$activeItem, @onActiveItemChanged
@subscribe @model, 'item-added', @onItemAdded
@subscribe @model, 'item-removed', @onItemRemoved
@subscribe @model, 'item-moved', @onItemMoved
@subscribe @model, 'before-item-destroyed', @onBeforeItemDestroyed
@subscribe @model, 'item-destroyed', @onItemDestroyed
@subscribe @model, 'activated', @onActivated
@subscribe @model.$active, @onActiveStatusChanged
@subscribe this, 'focusin', => @model.focus()
@subscribe this, 'focusout', => @model.blur()
@subscribe this, 'focus', =>
@activeView?.focus()
false
@command 'pane:save-items', => @saveItems()
@command 'pane:show-next-item', => @activateNextItem()
@command 'pane:show-previous-item', => @activatePreviousItem()
@command 'pane:show-item-1', => @activateItemAtIndex(0)
@command 'pane:show-item-2', => @activateItemAtIndex(1)
@command 'pane:show-item-3', => @activateItemAtIndex(2)
@command 'pane:show-item-4', => @activateItemAtIndex(3)
@command 'pane:show-item-5', => @activateItemAtIndex(4)
@command 'pane:show-item-6', => @activateItemAtIndex(5)
@command 'pane:show-item-7', => @activateItemAtIndex(6)
@command 'pane:show-item-8', => @activateItemAtIndex(7)
@command 'pane:show-item-9', => @activateItemAtIndex(8)
@command 'pane:split-left', => @splitLeft(@copyActiveItem())
@command 'pane:split-right', => @splitRight(@copyActiveItem())
@command 'pane:split-up', => @splitUp(@copyActiveItem())
@command 'pane:split-down', => @splitDown(@copyActiveItem())
@command 'pane:close', => @destroyItems()
@command 'pane:close-other-items', => @destroyInactiveItems()
deserializeParams: (params) ->
params.model = Pane.deserialize(params.model)
params
serializeParams: ->
model: @model.serialize()
# Deprecated: Use ::destroyItem
removeItem: (item) -> @destroyItem(item)
# Deprecated: Use ::activateItem
showItem: (item) -> @activateItem(item)
# Deprecated: Use ::activateItemForUri
showItemForUri: (item) -> @activateItemForUri(item)
# Deprecated: Use ::activateItemAtIndex
showItemAtIndex: (index) -> @activateItemAtIndex(index)
# Deprecated: Use ::activateNextItem
showNextItem: -> @activateNextItem()
# Deprecated: Use ::activatePreviousItem
showPreviousItem: -> @activatePreviousItem()
# Private:
afterAttach: (onDom) ->
@focus() if @model.focused and onDom
return if @attached
@attached = true
@trigger 'pane:attached', [this]
onActivated: =>
@focus() unless @hasFocus()
onActiveStatusChanged: (active) =>
if active
@addClass('active')
@trigger 'pane:became-active'
else
@removeClass('active')
@trigger 'pane:became-inactive'
# Public: Returns the next pane, ordered by creation.
getNextPane: ->
panes = @getContainer()?.getPanes()
return unless panes.length > 1
nextIndex = (panes.indexOf(this) + 1) % panes.length
panes[nextIndex]
getActivePaneItem: ->
@activeItem
onActiveItemChanged: (item) =>
@previousActiveItem?.off? 'title-changed', @activeItemTitleChanged
@previousActiveItem = item
return unless item?
hasFocus = @hasFocus()
item.on? 'title-changed', @activeItemTitleChanged
view = @viewForItem(item)
@itemViews.children().not(view).hide()
@itemViews.append(view) unless view.parent().is(@itemViews)
view.show() if @attached
view.focus() if hasFocus
@activeView = view
@trigger 'pane:active-item-changed', [item]
onItemAdded: (item, index) =>
@trigger 'pane:item-added', [item, index]
onItemRemoved: (item, index, destroyed) =>
if item instanceof $
viewToRemove = item
else if viewToRemove = @viewsByItem.get(item)
@viewsByItem.delete(item)
if viewToRemove?
viewToRemove.setModel?(null)
if destroyed
viewToRemove.remove()
else
viewToRemove.detach()
@trigger 'pane:item-removed', [item, index]
onItemMoved: (item, newIndex) =>
@trigger 'pane:item-moved', [item, newIndex]
onBeforeItemDestroyed: (item) =>
@unsubscribe(item) if typeof item.off is 'function'
@trigger 'pane:before-item-destroyed', [item]
onItemDestroyed: (item) =>
@getContainer()?.itemDestroyed(item)
# Private:
activeItemTitleChanged: =>
@trigger 'pane:active-item-title-changed'
# Private:
viewForItem: (item) ->
if item instanceof $
item
else if view = @viewsByItem.get(item)
view
else
viewClass = item.getViewClass()
view = new viewClass(item)
@viewsByItem.set(item, view)
view
# Private:
viewForActiveItem: ->
@viewForItem(@activeItem)
splitLeft: (items...) -> @model.splitLeft({items})._view
splitRight: (items...) -> @model.splitRight({items})._view
splitUp: (items...) -> @model.splitUp({items})._view
splitDown: (items...) -> @model.splitDown({items})._view
# Private:
getContainer: ->
@closest('.panes').view()
beforeRemove: ->
@model.destroy() unless @model.isDestroyed()
# Private:
remove: (selector, keepData) ->
return super if keepData
@unsubscribe()
super

View File

@ -1,226 +1,307 @@
{$, View} = require './space-pen-extensions'
{find, compact, extend} = require 'underscore-plus'
{dirname} = require 'path'
{Model, Sequence} = require 'theorist'
Serializable = require 'serializable'
Delegator = require 'delegato'
PaneAxis = require './pane-axis'
PaneView = null
PaneModel = require './pane-model'
# Public: A container which can contains multiple items to be switched between.
#
# Items can be almost anything however most commonly they're {EditorView}s.
#
# Most packages won't need to use this class, unless you're interested in
# building a package that deals with switching between panes or tiems.
# Public: A container for multiple items, one of which is *active* at a given
# time. With the default packages, a tab is displayed for each item and the
# active item's view is displayed.
module.exports =
class Pane extends View
class Pane extends Model
atom.deserializers.add(this)
Serializable.includeInto(this)
Delegator.includeInto(this)
@version: 1
@properties
container: null
activeItem: null
focused: false
@deserialize: (state) ->
new this(PaneModel.deserialize(state.model))
@content: (wrappedView) ->
@div class: 'pane', tabindex: -1, =>
@div class: 'item-views', outlet: 'itemViews'
@delegatesProperties 'items', 'activeItem', toProperty: 'model'
@delegatesMethods 'getItems', 'activateNextItem', 'activatePreviousItem', 'getActiveItemIndex',
'activateItemAtIndex', 'activateItem', 'addItem', 'itemAtIndex', 'moveItem', 'moveItemToPane',
'destroyItem', 'destroyItems', 'destroyActiveItem', 'destroyInactiveItems',
'saveActiveItem', 'saveActiveItemAs', 'saveItem', 'saveItemAs', 'saveItems',
'itemForUri', 'activateItemForUri', 'promptToSaveItem', 'copyActiveItem', 'isActive',
'activate', toProperty: 'model'
previousActiveItem: null
# Public: Only one pane is considered *active* at a time. A pane is activated
# when it is focused, and when focus returns to the pane container after
# moving to another element such as a panel, it returns to the active pane.
@behavior 'active', ->
@$container
.switch((container) -> container?.$activePane)
.map((activePane) => activePane is this)
.distinctUntilChanged()
# Private:
initialize: (args...) ->
if args[0] instanceof PaneModel
@model = args[0]
else
@model = new PaneModel(items: args)
@model._view = this
constructor: (params) ->
super
@onItemAdded(item) for item in @items
@viewsByItem = new WeakMap()
@handleEvents()
@items = Sequence.fromArray(params?.items ? [])
@activeItem ?= @items[0]
handleEvents: ->
@subscribe @model, 'destroyed', => @remove()
@subscribe @items.onEach (item) =>
if typeof item.on is 'function'
@subscribe item, 'destroyed', => @removeItem(item)
@subscribe @model.$activeItem, @onActiveItemChanged
@subscribe @model, 'item-added', @onItemAdded
@subscribe @model, 'item-removed', @onItemRemoved
@subscribe @model, 'item-moved', @onItemMoved
@subscribe @model, 'before-item-destroyed', @onBeforeItemDestroyed
@subscribe @model, 'item-destroyed', @onItemDestroyed
@subscribe @model, 'activated', @onActivated
@subscribe @model.$active, @onActiveStatusChanged
@subscribe @items.onRemoval (item, index) =>
@unsubscribe item if typeof item.on is 'function'
@subscribe this, 'focusin', => @model.focus()
@subscribe this, 'focusout', => @model.blur()
@subscribe this, 'focus', =>
@activeView?.focus()
false
@activate() if params?.active
@command 'pane:save-items', => @saveItems()
@command 'pane:show-next-item', => @activateNextItem()
@command 'pane:show-previous-item', => @activatePreviousItem()
@command 'pane:show-item-1', => @activateItemAtIndex(0)
@command 'pane:show-item-2', => @activateItemAtIndex(1)
@command 'pane:show-item-3', => @activateItemAtIndex(2)
@command 'pane:show-item-4', => @activateItemAtIndex(3)
@command 'pane:show-item-5', => @activateItemAtIndex(4)
@command 'pane:show-item-6', => @activateItemAtIndex(5)
@command 'pane:show-item-7', => @activateItemAtIndex(6)
@command 'pane:show-item-8', => @activateItemAtIndex(7)
@command 'pane:show-item-9', => @activateItemAtIndex(8)
@command 'pane:split-left', => @splitLeft(@copyActiveItem())
@command 'pane:split-right', => @splitRight(@copyActiveItem())
@command 'pane:split-up', => @splitUp(@copyActiveItem())
@command 'pane:split-down', => @splitDown(@copyActiveItem())
@command 'pane:close', => @destroyItems()
@command 'pane:close-other-items', => @destroyInactiveItems()
# Private: Called by the Serializable mixin during serialization.
serializeParams: ->
items: compact(@items.map((item) -> item.serialize?()))
activeItemUri: @activeItem?.getUri?()
focused: @focused
active: @active
# Private: Called by the Serializable mixin during deserialization.
deserializeParams: (params) ->
params.model = PaneModel.deserialize(params.model)
{items, activeItemUri} = params
params.items = compact(items.map (itemState) -> atom.deserializers.deserialize(itemState))
params.activeItem = find params.items, (item) -> item.getUri?() is activeItemUri
params
serializeParams: ->
model: @model.serialize()
# Private: Called by the view layer to construct a view for this model.
getViewClass: -> PaneView ?= require './pane-view'
# Deprecated: Use ::destroyItem
removeItem: (item) -> @destroyItem(item)
isActive: -> @active
# Deprecated: Use ::activateItem
showItem: (item) -> @activateItem(item)
# Private: Called by the view layer to indicate that the pane has gained focus.
focus: ->
@focused = true
@activate() unless @isActive()
# Deprecated: Use ::activateItemForUri
showItemForUri: (item) -> @activateItemForUri(item)
# Private: Called by the view layer to indicate that the pane has lost focus.
blur: ->
@focused = false
true # if this is called from an event handler, don't cancel it
# Deprecated: Use ::activateItemAtIndex
showItemAtIndex: (index) -> @activateItemAtIndex(index)
# Deprecated: Use ::activateNextItem
showNextItem: -> @activateNextItem()
# Deprecated: Use ::activatePreviousItem
showPreviousItem: -> @activatePreviousItem()
# Public: Makes this pane the *active* pane, causing it to gain focus
# immediately.
activate: ->
@container?.activePane = this
@emit 'activated'
# Private:
afterAttach: (onDom) ->
@focus() if @model.focused and onDom
getPanes: -> [this]
return if @attached
@attached = true
@trigger 'pane:attached', [this]
# Public:
getItems: ->
@items.slice()
onActivated: =>
@focus() unless @hasFocus()
# Public: Returns the item at the specified index.
itemAtIndex: (index) ->
@items[index]
onActiveStatusChanged: (active) =>
if active
@addClass('active')
@trigger 'pane:became-active'
# Public: Makes the next item active.
activateNextItem: ->
index = @getActiveItemIndex()
if index < @items.length - 1
@activateItemAtIndex(index + 1)
else
@removeClass('active')
@trigger 'pane:became-inactive'
@activateItemAtIndex(0)
# Public: Returns the next pane, ordered by creation.
getNextPane: ->
panes = @getContainer()?.getPanes()
return unless panes.length > 1
nextIndex = (panes.indexOf(this) + 1) % panes.length
panes[nextIndex]
getActivePaneItem: ->
@activeItem
onActiveItemChanged: (item) =>
@previousActiveItem?.off? 'title-changed', @activeItemTitleChanged
@previousActiveItem = item
return unless item?
hasFocus = @hasFocus()
item.on? 'title-changed', @activeItemTitleChanged
view = @viewForItem(item)
@itemViews.children().not(view).hide()
@itemViews.append(view) unless view.parent().is(@itemViews)
view.show() if @attached
view.focus() if hasFocus
@activeView = view
@trigger 'pane:active-item-changed', [item]
onItemAdded: (item, index) =>
@trigger 'pane:item-added', [item, index]
onItemRemoved: (item, index, destroyed) =>
if item instanceof $
viewToRemove = item
else if viewToRemove = @viewsByItem.get(item)
@viewsByItem.delete(item)
if viewToRemove?
viewToRemove.setModel?(null)
if destroyed
viewToRemove.remove()
else
viewToRemove.detach()
@trigger 'pane:item-removed', [item, index]
onItemMoved: (item, newIndex) =>
@trigger 'pane:item-moved', [item, newIndex]
onBeforeItemDestroyed: (item) =>
@unsubscribe(item) if typeof item.off is 'function'
@trigger 'pane:before-item-destroyed', [item]
onItemDestroyed: (item) =>
@getContainer()?.itemDestroyed(item)
# Private:
activeItemTitleChanged: =>
@trigger 'pane:active-item-title-changed'
# Private:
viewForItem: (item) ->
if item instanceof $
item
else if view = @viewsByItem.get(item)
view
# Public: Makes the previous item active.
activatePreviousItem: ->
index = @getActiveItemIndex()
if index > 0
@activateItemAtIndex(index - 1)
else
viewClass = item.getViewClass()
view = new viewClass(item)
@viewsByItem.set(item, view)
view
@activateItemAtIndex(@items.length - 1)
# Public: Returns the index of the current active item.
getActiveItemIndex: ->
@items.indexOf(@activeItem)
# Public: Makes the item at the given index active.
activateItemAtIndex: (index) ->
@activateItem(@itemAtIndex(index))
# Public: Makes the given item active, adding the item if necessary.
activateItem: (item) ->
if item?
@addItem(item)
@activeItem = item
# Public: Adds the item to the pane.
#
# * item:
# The item to add. It can be a model with an associated view or a view.
# * index:
# An optional index at which to add the item. If omitted, the item is
# added to the end.
#
# Returns the added item
addItem: (item, index=@getActiveItemIndex() + 1) ->
return if item in @items
@items.splice(index, 0, item)
@emit 'item-added', item, index
item
# Private:
viewForActiveItem: ->
@viewForItem(@activeItem)
removeItem: (item, destroying) ->
index = @items.indexOf(item)
return if index is -1
@activateNextItem() if item is @activeItem and @items.length > 1
@items.splice(index, 1)
@emit 'item-removed', item, index, destroying
@destroy() if @items.length is 0
splitLeft: (items...) -> @model.splitLeft({items})._view
# Public: Moves the given item to the specified index.
moveItem: (item, newIndex) ->
oldIndex = @items.indexOf(item)
@items.splice(oldIndex, 1)
@items.splice(newIndex, 0, item)
@emit 'item-moved', item, newIndex
splitRight: (items...) -> @model.splitRight({items})._view
# Public: Moves the given item to the given index at another pane.
moveItemToPane: (item, pane, index) ->
pane.addItem(item, index)
@removeItem(item)
splitUp: (items...) -> @model.splitUp({items})._view
# Public: Destroys the currently active item and make the next item active.
destroyActiveItem: ->
@destroyItem(@activeItem)
false
splitDown: (items...) -> @model.splitDown({items})._view
# Public: Destroys the given item. If it is the active item, activate the next
# one. If this is the last item, also destroys the pane.
destroyItem: (item) ->
@emit 'before-item-destroyed', item
if @promptToSaveItem(item)
@emit 'item-destroyed', item
@removeItem(item, true)
item.destroy?()
true
else
false
# Public: Destroys all items and destroys the pane.
destroyItems: ->
@destroyItem(item) for item in @getItems()
# Public: Destroys all items but the active one.
destroyInactiveItems: ->
@destroyItem(item) for item in @getItems() when item isnt @activeItem
# Private: Called by model superclass.
destroyed: ->
@container.activateNextPane() if @isActive()
item.destroy?() for item in @items.slice()
# Public: Prompts the user to save the given item if it can be saved and is
# currently unsaved.
promptToSaveItem: (item) ->
return true unless item.shouldPromptToSave?()
uri = item.getUri()
chosen = atom.confirm
message: "'#{item.getTitle?() ? item.getUri()}' has changes, do you want to save them?"
detailedMessage: "Your changes will be lost if you close this item without saving."
buttons: ["Save", "Cancel", "Don't Save"]
switch chosen
when 0 then @saveItem(item, -> true)
when 1 then false
when 2 then true
# Public: Saves the active item.
saveActiveItem: ->
@saveItem(@activeItem)
# Public: Saves the active item at a prompted-for location.
saveActiveItemAs: ->
@saveItemAs(@activeItem)
# Public: Saves the specified item.
#
# * item: The item to save.
# * nextAction: An optional function which will be called after the item is saved.
saveItem: (item, nextAction) ->
if item.getUri?()
item.save?()
nextAction?()
else
@saveItemAs(item, nextAction)
# Public: Saves the given item at a prompted-for location.
#
# * item: The item to save.
# * nextAction: An optional function which will be called after the item is saved.
saveItemAs: (item, nextAction) ->
return unless item.saveAs?
itemPath = item.getPath?()
itemPath = dirname(itemPath) if itemPath
path = atom.showSaveDialogSync(itemPath)
if path
item.saveAs(path)
nextAction?()
# Public: Saves all items.
saveItems: ->
@saveItem(item) for item in @getItems()
# Public: Returns the first item that matches the given URI or undefined if
# none exists.
itemForUri: (uri) ->
find @items, (item) -> item.getUri?() is uri
# Public: Activates the first item that matches the given URI. Returns a
# boolean indicating whether a matching item was found.
activateItemForUri: (uri) ->
if item = @itemForUri(uri)
@activateItem(item)
true
else
false
# Private:
getContainer: ->
@closest('.panes').view()
copyActiveItem: ->
@activeItem.copy?() ? atom.deserializers.deserialize(@activeItem.serialize())
beforeRemove: ->
@model.destroy() unless @model.isDestroyed()
# Public: Creates a new pane to the left of the receiver.
#
# * params:
# + items: An optional array of items with which to construct the new pane.
#
# Returns the new {Pane}.
splitLeft: (params) ->
@split('horizontal', 'before', params)
# Public: Creates a new pane to the right of the receiver.
#
# * params:
# + items: An optional array of items with which to construct the new pane.
#
# Returns the new {Pane}.
splitRight: (params) ->
@split('horizontal', 'after', params)
# Public: Creates a new pane above the receiver.
#
# * params:
# + items: An optional array of items with which to construct the new pane.
#
# Returns the new {Pane}.
splitUp: (params) ->
@split('vertical', 'before', params)
# Public: Creates a new pane below the receiver.
#
# * params:
# + items: An optional array of items with which to construct the new pane.
#
# Returns the new {Pane}.
splitDown: (params) ->
@split('vertical', 'after', params)
# Private:
remove: (selector, keepData) ->
return super if keepData
@unsubscribe()
super
split: (orientation, side, params) ->
if @parent.orientation isnt orientation
@parent.replaceChild(this, new PaneAxis({@container, orientation, children: [this]}))
newPane = new @constructor(extend({focused: true}, params))
switch side
when 'before' then @parent.insertChildBefore(this, newPane)
when 'after' then @parent.insertChildAfter(this, newPane)
newPane.activate()
newPane

View File

@ -6,10 +6,10 @@ _ = require 'underscore-plus'
fs = require 'fs-plus'
Serializable = require 'serializable'
EditorView = require './editor-view'
Pane = require './pane'
PaneColumn = require './pane-column'
PaneRow = require './pane-row'
PaneContainer = require './pane-container'
PaneView = require './pane-view'
PaneColumnView = require './pane-column-view'
PaneRowView = require './pane-row-view'
PaneContainerView = require './pane-container-view'
Editor = require './editor'
# Public: The container for the entire Atom application.
@ -39,7 +39,7 @@ Editor = require './editor'
module.exports =
class WorkspaceView extends View
Serializable.includeInto(this)
atom.deserializers.add(this, Pane, PaneRow, PaneColumn, EditorView)
atom.deserializers.add(this, PaneView, PaneRowView, PaneColumnView, EditorView)
@version: 2
@ -60,7 +60,7 @@ class WorkspaceView extends View
# Private:
initialize: ({panes, @fullScreen}={}) ->
panes ?= new PaneContainer
panes ?= new PaneContainerView
@panes.replaceWith(panes)
@panes = panes
@ -168,7 +168,7 @@ class WorkspaceView extends View
Q(editor ? promise)
.then (editor) =>
if not activePane
activePane = new Pane(editor)
activePane = new PaneView(editor)
@panes.setRoot(activePane)
@itemOpened(editor)
@ -203,7 +203,7 @@ class WorkspaceView extends View
pane.activateItem(paneItem)
else
paneItem = atom.project.openSync(uri, {initialLine})
pane = new Pane(paneItem)
pane = new PaneView(paneItem)
@panes.setRoot(pane)
@itemOpened(paneItem)
@ -290,11 +290,11 @@ class WorkspaceView extends View
appendToRight: (element) ->
@horizontal.append(element)
# Public: Returns the currently focused {Pane}.
# Public: Returns the currently focused {PaneView}.
getActivePane: ->
@panes.getActivePane()
# Public: Returns the currently focused item from within the focused {Pane}
# Public: Returns the currently focused item from within the focused {PaneView}
getActivePaneItem: ->
@panes.getActivePaneItem()
@ -329,15 +329,15 @@ class WorkspaceView extends View
saveAll: ->
@panes.saveAll()
# Public: Fires a callback on each open {Pane}.
# Public: Fires a callback on each open {PaneView}.
eachPane: (callback) ->
@panes.eachPane(callback)
# Public: Returns an Array of all open {Pane}s.
# Public: Returns an Array of all open {PaneView}s.
getPanes: ->
@panes.getPanes()
# Public: Return the id of the given a {Pane}
# Public: Return the id of the given a {PaneView}
indexOfPane: (pane) ->
@panes.indexOfPane(pane)