diff --git a/spec/app/pane-container-spec.coffee b/spec/app/pane-container-spec.coffee
index e69de29bb..8d2bf21e1 100644
--- a/spec/app/pane-container-spec.coffee
+++ b/spec/app/pane-container-spec.coffee
@@ -0,0 +1,47 @@
+PaneContainer = require 'pane-container'
+Pane = require 'pane'
+{View} = require 'space-pen'
+$ = require 'jquery'
+
+describe "PaneContainer", ->
+ [TestView, container, pane1, pane2, pane3] = []
+
+ beforeEach ->
+ class TestView extends View
+ registerDeserializer(this)
+ @deserialize: ({myText}) -> new TestView(myText)
+ @content: -> @div tabindex: -1
+ initialize: (@myText) -> @text(@myText)
+ serialize: -> deserializer: 'TestView', myText: @myText
+
+ container = new PaneContainer
+ pane1 = new Pane(new TestView('1'))
+ container.append(pane1)
+ pane2 = pane1.splitRight(new TestView('2'))
+ pane3 = pane2.splitDown(new TestView('3'))
+
+ afterEach ->
+ unregisterDeserializer(TestView)
+
+ describe ".focusNextPane()", ->
+ it "focuses the pane following the focused pane or the first pane if no pane has focus", ->
+ container.attachToDom()
+ container.focusNextPane()
+ expect(pane1.currentItem).toMatchSelector ':focus'
+ container.focusNextPane()
+ expect(pane2.currentItem).toMatchSelector ':focus'
+ container.focusNextPane()
+ expect(pane3.currentItem).toMatchSelector ':focus'
+ container.focusNextPane()
+ expect(pane1.currentItem).toMatchSelector ':focus'
+
+ describe "serialization", ->
+ it "can be serialized and deserialized, and correctly adjusts dimensions of deserialized panes after attach", ->
+ newContainer = deserialize(container.serialize())
+ expect(newContainer.find('.row > :contains(1)')).toExist()
+ expect(newContainer.find('.row > .column > :contains(2)')).toExist()
+ expect(newContainer.find('.row > .column > :contains(3)')).toExist()
+
+ newContainer.height(200).width(300).attachToDom()
+ expect(newContainer.find('.row > :contains(1)').width()).toBe 150
+ expect(newContainer.find('.row > .column > :contains(2)').height()).toBe 100
diff --git a/spec/app/pane-spec.coffee b/spec/app/pane-spec.coffee
index bdcc1d246..a856fcc82 100644
--- a/spec/app/pane-spec.coffee
+++ b/spec/app/pane-spec.coffee
@@ -1,12 +1,13 @@
-Editor = require 'editor'
+PaneContainer = require 'pane-container'
Pane = require 'pane'
{$$} = require 'space-pen'
+$ = require 'jquery'
describe "Pane", ->
[container, view1, view2, editSession1, editSession2, pane] = []
beforeEach ->
- container = $$ -> @div id: 'panes'
+ container = new PaneContainer
view1 = $$ -> @div id: 'view-1', 'View 1'
view2 = $$ -> @div id: 'view-2', 'View 2'
editSession1 = project.buildEditSession('sample.js')
@@ -52,7 +53,7 @@ describe "Pane", ->
expect(editor.activeEditSession).toBe editSession2
describe "when showing a view item", ->
- it "appends it to the itemViews div if it hasn't already been appended and show it", ->
+ it "appends it to the itemViews div if it hasn't already been appended and shows it", ->
expect(pane.itemViews.find('#view-2')).not.toExist()
pane.showItem(view2)
expect(pane.itemViews.find('#view-2')).toExist()
@@ -112,8 +113,9 @@ describe "Pane", ->
expect(focusHandler).toHaveBeenCalled()
describe "split methods", ->
- [view3, view4] = []
+ [pane1, view3, view4] = []
beforeEach ->
+ pane1 = pane
pane.showItem(editSession1)
view3 = $$ -> @div id: 'view-3', 'View 3'
view4 = $$ -> @div id: 'view-4', 'View 4'
@@ -121,8 +123,8 @@ describe "Pane", ->
describe "splitRight(items...)", ->
it "builds a row if needed, then appends a new pane after itself", ->
# creates the new pane with a copy of the current item if none are given
- pane2 = pane.splitRight()
- expect(container.find('.row .pane').toArray()).toEqual [pane[0], pane2[0]]
+ pane2 = pane1.splitRight()
+ expect(container.find('.row .pane').toArray()).toEqual [pane1[0], pane2[0]]
expect(pane2.items).toEqual [editSession1]
expect(pane2.currentItem).not.toBe editSession1 # it's a copy
@@ -166,6 +168,75 @@ describe "Pane", ->
expect(pane3.getItems()).toEqual [view3, view4]
expect(container.find('.column .pane').toArray()).toEqual [pane3[0], pane2[0], pane[0]]
+ it "lays out nested panes by equally dividing their containing row / column", ->
+ container.width(520).height(240).attachToDom()
+ pane1.showItem($("1"))
+ pane1
+ .splitLeft($("2"))
+ .splitUp($("3"))
+ .splitLeft($("4"))
+ .splitDown($("5"))
+
+ row1 = container.children(':eq(0)')
+ expect(row1.children().length).toBe 2
+ column1 = row1.children(':eq(0)').view()
+ pane1 = row1.children(':eq(1)').view()
+ expect(column1.outerWidth()).toBe Math.round(2/3 * container.width())
+ expect(column1.outerHeight()).toBe container.height()
+ expect(pane1.outerWidth()).toBe Math.round(1/3 * container.width())
+ expect(pane1.outerHeight()).toBe container.height()
+ expect(Math.round(pane1.position().left)).toBe column1.outerWidth()
+
+ expect(column1.children().length).toBe 2
+ row2 = column1.children(':eq(0)').view()
+ pane2 = column1.children(':eq(1)').view()
+ expect(row2.outerWidth()).toBe column1.outerWidth()
+ expect(row2.height()).toBe 2/3 * container.height()
+ expect(pane2.outerWidth()).toBe column1.outerWidth()
+ expect(pane2.outerHeight()).toBe 1/3 * container.height()
+ expect(pane2.position().top).toBe row2.height()
+
+ expect(row2.children().length).toBe 2
+ column3 = row2.children(':eq(0)').view()
+ pane3 = row2.children(':eq(1)').view()
+ expect(column3.outerWidth()).toBe Math.round(1/3 * container.width())
+ expect(column3.outerHeight()).toBe row2.outerHeight()
+ # the built in rounding seems to be rounding x.5 down, but we need to go up. this sucks.
+ expect(Math.round(pane3.trueWidth())).toBe Math.round(1/3 * container.width())
+ expect(pane3.height()).toBe row2.outerHeight()
+ expect(Math.round(pane3.position().left)).toBe column3.width()
+
+ expect(column3.children().length).toBe 2
+ pane4 = column3.children(':eq(0)').view()
+ pane5 = column3.children(':eq(1)').view()
+ expect(pane4.outerWidth()).toBe column3.width()
+ expect(pane4.outerHeight()).toBe 1/3 * container.height()
+ expect(pane5.outerWidth()).toBe column3.width()
+ expect(pane5.position().top).toBe pane4.outerHeight()
+ expect(pane5.outerHeight()).toBe 1/3 * container.height()
+
+ pane5.remove()
+ expect(column3.parent()).not.toExist()
+ expect(pane2.outerHeight()).toBe Math.floor(1/2 * container.height())
+ expect(pane3.outerHeight()).toBe Math.floor(1/2 * container.height())
+ expect(pane4.outerHeight()).toBe Math.floor(1/2 * container.height())
+
+ pane4.remove()
+ expect(row2.parent()).not.toExist()
+ expect(pane1.outerWidth()).toBe Math.floor(1/2 * container.width())
+ expect(pane2.outerWidth()).toBe Math.floor(1/2 * container.width())
+ expect(pane3.outerWidth()).toBe Math.floor(1/2 * container.width())
+
+ pane3.remove()
+ expect(column1.parent()).not.toExist()
+ expect(pane2.outerHeight()).toBe container.height()
+
+ pane2.remove()
+ expect(row1.parent()).not.toExist()
+ expect(container.children().length).toBe 1
+ expect(container.children('.pane').length).toBe 1
+ expect(pane1.outerWidth()).toBe container.width()
+
describe ".itemForPath(path)", ->
it "returns the item for which a call to .getPath() returns the given path", ->
expect(pane.itemForPath(editSession1.getPath())).toBe editSession1
diff --git a/spec/app/root-view-spec.coffee b/spec/app/root-view-spec.coffee
index 5be73e8e9..8b1d6f691 100644
--- a/spec/app/root-view-spec.coffee
+++ b/spec/app/root-view-spec.coffee
@@ -171,190 +171,6 @@ describe "RootView", ->
rootView.focusNextPane()
expect(view1.focus).toHaveBeenCalled()
- describe "pane layout", ->
- beforeEach ->
- rootView.attachToDom()
- rootView.width(800)
- rootView.height(600)
- pane1.attr('id', 'pane-1')
- newPaneContent = $("
New pane content
")
- spyOn(newPaneContent, 'focus')
-
- describe "vertical splits", ->
- describe "when .splitRight(view) is called on a pane", ->
- it "places a new pane to the right of the current pane in a .row div", ->
- expect(rootView.panes.find('.row')).not.toExist()
-
- pane2 = pane1.splitRight(newPaneContent)
- expect(newPaneContent.focus).toHaveBeenCalled()
-
- expect(rootView.panes.find('.row')).toExist()
- expect(rootView.panes.find('.row .pane').length).toBe 2
- [leftPane, rightPane] = rootView.panes.find('.row .pane').map -> $(this).view()
- expect(rightPane[0]).toBe pane2[0]
- expect(leftPane.attr('id')).toBe 'pane-1'
- expect(rightPane.currentItem).toBe newPaneContent
-
- expectedColumnWidth = Math.floor(rootView.panes.width() / 2)
- expect(leftPane.outerWidth()).toBe expectedColumnWidth
- expect(rightPane.position().left).toBe expectedColumnWidth
- expect(rightPane.outerWidth()).toBe expectedColumnWidth
-
- pane2.remove()
-
- expect(rootView.panes.find('.row')).not.toExist()
- expect(rootView.panes.find('.pane').length).toBe 1
- expect(pane1.outerWidth()).toBe rootView.panes.width()
-
- describe "when splitLeft(view) is called on a pane", ->
- it "places a new pane to the left of the current pane in a .row div", ->
- expect(rootView.find('.row')).not.toExist()
-
- pane2 = pane1.splitLeft(newPaneContent)
- expect(newPaneContent.focus).toHaveBeenCalled()
-
- expect(rootView.find('.row')).toExist()
- expect(rootView.find('.row .pane').length).toBe 2
- [leftPane, rightPane] = rootView.find('.row .pane').map -> $(this).view()
- expect(leftPane[0]).toBe pane2[0]
- expect(rightPane.attr('id')).toBe 'pane-1'
- expect(leftPane.currentItem).toBe
-
- expectedColumnWidth = Math.floor(rootView.panes.width() / 2)
- expect(leftPane.outerWidth()).toBe expectedColumnWidth
- expect(rightPane.position().left).toBe expectedColumnWidth
- expect(rightPane.outerWidth()).toBe expectedColumnWidth
-
- pane2.remove()
-
- expect(rootView.panes.find('.row')).not.toExist()
- expect(rootView.panes.find('.pane').length).toBe 1
- expect(pane1.outerWidth()).toBe rootView.panes.width()
- expect(pane1.position().left).toBe 0
-
- describe "horizontal splits", ->
- describe "when splitUp(view) is called on a pane", ->
- it "places a new pane above the current pane in a .column div", ->
- expect(rootView.find('.column')).not.toExist()
-
- pane2 = pane1.splitUp(newPaneContent)
- expect(newPaneContent.focus).toHaveBeenCalled()
-
- expect(rootView.find('.column')).toExist()
- expect(rootView.find('.column .pane').length).toBe 2
- [topPane, bottomPane] = rootView.find('.column .pane').map -> $(this).view()
- expect(topPane[0]).toBe pane2[0]
- expect(bottomPane.attr('id')).toBe 'pane-1'
- expect(topPane.currentItem).toBe newPaneContent
-
- expectedRowHeight = Math.floor(rootView.panes.height() / 2)
- expect(topPane.outerHeight()).toBe expectedRowHeight
- expect(bottomPane.position().top).toBe expectedRowHeight
- expect(bottomPane.outerHeight()).toBe expectedRowHeight
-
- pane2.remove()
-
- expect(rootView.panes.find('.column')).not.toExist()
- expect(rootView.panes.find('.pane').length).toBe 1
- expect(pane1.outerHeight()).toBe rootView.panes.height()
- expect(pane1.position().top).toBe 0
-
- describe "when splitDown(view) is called on a pane", ->
- it "places a new pane below the current pane in a .column div", ->
- expect(rootView.find('.column')).not.toExist()
-
- pane2 = pane1.splitDown(newPaneContent)
- expect(newPaneContent.focus).toHaveBeenCalled()
-
- expect(rootView.find('.column')).toExist()
- expect(rootView.find('.column .pane').length).toBe 2
- [topPane, bottomPane] = rootView.find('.column .pane').map -> $(this).view()
- expect(bottomPane[0]).toBe pane2[0]
- expect(topPane.attr('id')).toBe 'pane-1'
- expect(bottomPane.currentItem).toBe newPaneContent
-
- expectedRowHeight = Math.floor(rootView.panes.height() / 2)
- expect(topPane.outerHeight()).toBe expectedRowHeight
- expect(bottomPane.position().top).toBe expectedRowHeight
- expect(bottomPane.outerHeight()).toBe expectedRowHeight
-
- pane2.remove()
-
- expect(rootView.panes.find('.column')).not.toExist()
- expect(rootView.panes.find('.pane').length).toBe 1
- expect(pane1.outerHeight()).toBe rootView.panes.height()
-
- describe "layout of nested vertical and horizontal splits", ->
- it "lays out rows and columns with a consistent width", ->
- pane1.showItem($("1"))
-
- pane1
- .splitLeft($("2"))
- .splitUp($("3"))
- .splitLeft($("4"))
- .splitDown($("5"))
-
- row1 = rootView.panes.children(':eq(0)')
- expect(row1.children().length).toBe 2
- column1 = row1.children(':eq(0)').view()
- pane1 = row1.children(':eq(1)').view()
- expect(column1.outerWidth()).toBe Math.round(2/3 * rootView.panes.width())
- expect(column1.outerHeight()).toBe rootView.height()
- expect(pane1.outerWidth()).toBe Math.round(1/3 * rootView.panes.width())
- expect(pane1.outerHeight()).toBe rootView.height()
- expect(Math.round(pane1.position().left)).toBe column1.outerWidth()
-
- expect(column1.children().length).toBe 2
- row2 = column1.children(':eq(0)').view()
- pane2 = column1.children(':eq(1)').view()
- expect(row2.outerWidth()).toBe column1.outerWidth()
- expect(row2.height()).toBe 2/3 * rootView.panes.height()
- expect(pane2.outerWidth()).toBe column1.outerWidth()
- expect(pane2.outerHeight()).toBe 1/3 * rootView.panes.height()
- expect(pane2.position().top).toBe row2.height()
-
- expect(row2.children().length).toBe 2
- column3 = row2.children(':eq(0)').view()
- pane3 = row2.children(':eq(1)').view()
- expect(column3.outerWidth()).toBe Math.round(1/3 * rootView.panes.width())
- expect(column3.outerHeight()).toBe row2.outerHeight()
- # the built in rounding seems to be rounding x.5 down, but we need to go up. this sucks.
- expect(Math.round(pane3.trueWidth())).toBe Math.round(1/3 * rootView.panes.width())
- expect(pane3.height()).toBe row2.outerHeight()
- expect(Math.round(pane3.position().left)).toBe column3.width()
-
- expect(column3.children().length).toBe 2
- pane4 = column3.children(':eq(0)').view()
- pane5 = column3.children(':eq(1)').view()
- expect(pane4.outerWidth()).toBe column3.width()
- expect(pane4.outerHeight()).toBe 1/3 * rootView.panes.height()
- expect(pane5.outerWidth()).toBe column3.width()
- expect(pane5.position().top).toBe pane4.outerHeight()
- expect(pane5.outerHeight()).toBe 1/3 * rootView.panes.height()
-
- pane5.remove()
-
- expect(column3.parent()).not.toExist()
- expect(pane2.outerHeight()).toBe Math.floor(1/2 * rootView.panes.height())
- expect(pane3.outerHeight()).toBe Math.floor(1/2 * rootView.panes.height())
- expect(pane4.outerHeight()).toBe Math.floor(1/2 * rootView.panes.height())
-
- pane4.remove()
- expect(row2.parent()).not.toExist()
- expect(pane1.outerWidth()).toBe Math.floor(1/2 * rootView.panes.width())
- expect(pane2.outerWidth()).toBe Math.floor(1/2 * rootView.panes.width())
- expect(pane3.outerWidth()).toBe Math.floor(1/2 * rootView.panes.width())
-
- pane3.remove()
- expect(column1.parent()).not.toExist()
- expect(pane2.outerHeight()).toBe rootView.panes.height()
-
- pane2.remove()
- expect(row1.parent()).not.toExist()
- expect(rootView.panes.children().length).toBe 1
- expect(rootView.panes.children('.pane').length).toBe 1
- expect(pane1.outerWidth()).toBe rootView.panes.width()
-
describe "keymap wiring", ->
commandHandler = null
beforeEach ->
diff --git a/src/app/pane-container.coffee b/src/app/pane-container.coffee
new file mode 100644
index 000000000..87bc09f98
--- /dev/null
+++ b/src/app/pane-container.coffee
@@ -0,0 +1,41 @@
+{View} = require 'space-pen'
+$ = require 'jquery'
+
+module.exports =
+class PaneContainer extends View
+ registerDeserializer(this)
+
+ @deserialize: ({root}) ->
+ container = new PaneContainer
+ container.append(deserialize(root)) if root
+ container
+
+ @content: ->
+ @div id: 'panes'
+
+ serialize: ->
+ deserializer: 'PaneContainer'
+ root: @getRoot()?.serialize()
+
+ focusNextPane: ->
+ panes = @getPanes()
+ currentIndex = panes.indexOf(@getFocusedPane())
+ nextIndex = (currentIndex + 1) % panes.length
+ panes[nextIndex].focus()
+
+ getRoot: ->
+ @children().first().view()
+
+ getPanes: ->
+ @find('.pane').toArray().map (node)-> $(node).view()
+
+ getFocusedPane: ->
+ @find('.pane:has(:focus)').view()
+
+ adjustPaneDimensions: ->
+ if root = @getRoot()
+ root.css(width: '100%', height: '100%', top: 0, left: 0)
+ root.adjustDimensions()
+
+ afterAttach: ->
+ @adjustPaneDimensions()
diff --git a/src/app/pane.coffee b/src/app/pane.coffee
index b2c1ca5e1..ebad81482 100644
--- a/src/app/pane.coffee
+++ b/src/app/pane.coffee
@@ -125,27 +125,31 @@ class Pane extends View
items = [@copyCurrentItem()] unless items.length
pane = new Pane(items...)
this[side](pane)
- rootView?.adjustPaneDimensions()
+ @getContainer().adjustPaneDimensions()
pane.focus()
pane
+ buildPaneAxis: (axis) ->
+ switch axis
+ when 'row' then new PaneRow
+ when 'column' then new PaneColumn
+
+ getContainer: ->
+ @closest('#panes').view()
+
copyCurrentItem: ->
deserialize(@currentItem.serialize())
remove: (selector, keepData) ->
return super if keepData
# find parent elements before removing from dom
+ container = @getContainer()
parentAxis = @parent('.row, .column')
super
if parentAxis.children().length == 1
sibling = parentAxis.children().detach()
parentAxis.replaceWith(sibling)
- rootView?.adjustPaneDimensions()
+ container.adjustPaneDimensions()
afterRemove: ->
item.destroy?() for item in @getItems()
-
- buildPaneAxis: (axis) ->
- switch axis
- when 'row' then new PaneRow
- when 'column' then new PaneColumn
diff --git a/src/app/root-view.coffee b/src/app/root-view.coffee
index 7f8a72858..a557fa866 100644
--- a/src/app/root-view.coffee
+++ b/src/app/root-view.coffee
@@ -10,6 +10,7 @@ Project = require 'project'
Pane = require 'pane'
PaneColumn = require 'pane-column'
PaneRow = require 'pane-row'
+PaneContainer = require 'pane-container'
module.exports =
class RootView extends View
@@ -19,17 +20,16 @@ class RootView extends View
ignoredNames: [".git", ".svn", ".DS_Store"]
disabledPackages: []
- @content: ->
+ @content: ({panes}) ->
@div id: 'root-view', =>
@div id: 'horizontal', outlet: 'horizontal', =>
@div id: 'vertical', outlet: 'vertical', =>
- @div id: 'panes', outlet: 'panes'
+ @subview 'panes', panes ? new PaneContainer
@deserialize: ({ panesViewState, packageStates, projectPath }) ->
atom.atomPackageStates = packageStates ? {}
- rootView = new RootView
- rootView.setRootPane(deserialize(panesViewState)) if panesViewState
- rootView
+ panes = deserialize(panesViewState) if panesViewState?.deserializer is 'PaneContainer'
+ new RootView({panes})
title: null
@@ -67,7 +67,7 @@ class RootView extends View
serialize: ->
deserializer: 'RootView'
- panesViewState: @panes.children().view()?.serialize()
+ panesViewState: @panes.serialize()
packageStates: atom.serializeAtomPackages()
handleFocus: (e) ->
@@ -170,24 +170,8 @@ class RootView extends View
getActiveEditSession: ->
@getActiveEditor()?.activeEditSession
- focusNextPane: ->
- panes = @panes.find('.pane')
- currentIndex = panes.toArray().indexOf(@getFocusedPane()[0])
- nextIndex = (currentIndex + 1) % panes.length
- panes.eq(nextIndex).view().focus()
-
- getFocusedPane: ->
- @panes.find('.pane:has(:focus)')
-
- setRootPane: (pane) ->
- @panes.empty()
- @panes.append(pane)
- @adjustPaneDimensions()
-
- adjustPaneDimensions: ->
- rootPane = @panes.children().first().view()
- rootPane?.css(width: '100%', height: '100%', top: 0, left: 0)
- rootPane?.adjustDimensions()
+ focusNextPane: -> @panes.focusNextPane()
+ getFocusedPane: -> @panes.getFocusedPane()
remove: ->
editor.remove() for editor in @getEditors()
diff --git a/src/app/window.coffee b/src/app/window.coffee
index a4c514b15..2ff52830c 100644
--- a/src/app/window.coffee
+++ b/src/app/window.coffee
@@ -151,6 +151,9 @@ window.registerDeserializers = (args...) ->
window.registerDeserializer = (klass) ->
deserializers[klass.name] = klass
+window.unregisterDeserializer = (klass) ->
+ delete deserializers[klass.name]
+
window.deserialize = (state) ->
deserializers[state?.deserializer]?.deserialize(state)
diff --git a/static/atom.css b/static/atom.css
index 003390b38..6cfb79269 100644
--- a/static/atom.css
+++ b/static/atom.css
@@ -21,12 +21,12 @@ html, body {
-webkit-flex-flow: column;
}
-#root-view #panes {
+#panes {
position: relative;
-webkit-flex: 1;
}
-#root-view #panes .column {
+#panes .column {
position: absolute;
top: 0;
bottom: 0;
@@ -35,7 +35,7 @@ html, body {
overflow-y: hidden;
}
-#root-view #panes .row {
+#panes .row {
position: absolute;
top: 0;
bottom: 0;
@@ -44,7 +44,7 @@ html, body {
overflow-x: hidden;
}
-#root-view #panes .pane {
+#panes .pane {
position: absolute;
display: -webkit-flex;
-webkit-flex-flow: column;
@@ -55,7 +55,7 @@ html, body {
box-sizing: border-box;
}
-#root-view #panes .pane .item-views {
+#panes .pane .item-views {
-webkit-flex: 1;
display: -webkit-flex;
-webkit-flex-flow: column;