Add a PaneContainer subview for RootView

PaneContainer is responsible for all pane-related logic. Laying them
out, switching focus between them, etc. This should help make RootView
simpler and keep pane-layout related tests in their own focused area.
This commit is contained in:
Nathan Sobo
2013-02-20 22:28:43 -07:00
committed by probablycorey
parent 9ecb03e470
commit fee835f899
8 changed files with 192 additions and 226 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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 = $("<div>New pane content</div>")
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 ->

View File

@@ -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()

View File

@@ -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

View File

@@ -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()

View File

@@ -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)

View File

@@ -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;