From fee835f8995236112f5d64c3f2c86aea4c08d0f0 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 20 Feb 2013 22:28:43 -0700 Subject: [PATCH] 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. --- spec/app/pane-container-spec.coffee | 47 +++++++ spec/app/pane-spec.coffee | 83 ++++++++++++- spec/app/root-view-spec.coffee | 184 ---------------------------- src/app/pane-container.coffee | 41 +++++++ src/app/pane.coffee | 18 +-- src/app/root-view.coffee | 32 ++--- src/app/window.coffee | 3 + static/atom.css | 10 +- 8 files changed, 192 insertions(+), 226 deletions(-) create mode 100644 src/app/pane-container.coffee 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;