Add commands to move directionally between panes

This commit is contained in:
Jason Rudolph
2014-01-26 17:25:59 -05:00
parent 2b34b2b9ba
commit dd5c65b5f9
4 changed files with 313 additions and 0 deletions

View File

@@ -265,3 +265,89 @@ describe "PaneContainerView", ->
pane1.remove()
pane2.remove()
expect(activeItemChangedHandler).not.toHaveBeenCalled()
describe "changing focus directionally between panes", ->
[pane1, pane2, pane3, pane4, pane5, pane6, pane7, pane8, pane9] = []
beforeEach ->
# Set up a grid of 9 panes, in the following arrangement, where the
# numbers correspond to the variable names below.
#
# -------
# |1|2|3|
# -------
# |4|5|6|
# -------
# |7|8|9|
# -------
container = new PaneContainerView
pane1 = container.getRoot()
pane1.activateItem(new TestView('1'))
pane4 = pane1.splitDown(new TestView('4'))
pane7 = pane4.splitDown(new TestView('7'))
pane2 = pane1.splitRight(new TestView('2'))
pane3 = pane2.splitRight(new TestView('3'))
pane5 = pane4.splitRight(new TestView('5'))
pane6 = pane5.splitRight(new TestView('6'))
pane8 = pane7.splitRight(new TestView('8'))
pane9 = pane8.splitRight(new TestView('9'))
container.height(400)
container.width(400)
container.attachToDom()
describe ".focusPaneAbove()", ->
describe "when there are multiple rows above the focused pane", ->
it "focuses up to the adjacent row", ->
pane8.focus()
container.focusPaneAbove()
expect(pane5.activeItem).toMatchSelector ':focus'
describe "when there are no rows above the focused pane", ->
it "keeps the current pane focused", ->
pane2.focus()
container.focusPaneAbove()
expect(pane2.activeItem).toMatchSelector ':focus'
describe ".focusPaneBelow()", ->
describe "when there are multiple rows below the focused pane", ->
it "focuses down to the adjacent row", ->
pane2.focus()
container.focusPaneBelow()
expect(pane5.activeItem).toMatchSelector ':focus'
describe "when there are no rows below the focused pane", ->
it "keeps the current pane focused", ->
pane8.focus()
container.focusPaneBelow()
expect(pane8.activeItem).toMatchSelector ':focus'
describe ".focusPaneOnLeft()", ->
describe "when there are multiple columns to the left of the focused pane", ->
it "focuses left to the adjacent column", ->
pane6.focus()
container.focusPaneOnLeft()
expect(pane5.activeItem).toMatchSelector ':focus'
describe "when there are no columns to the left of the focused pane", ->
it "keeps the current pane focused", ->
pane4.focus()
container.focusPaneOnLeft()
expect(pane4.activeItem).toMatchSelector ':focus'
describe ".focusPaneOnRight()", ->
describe "when there are multiple columns to the right of the focused pane", ->
it "focuses right to the adjacent column", ->
pane4.focus()
container.focusPaneOnRight()
expect(pane5.activeItem).toMatchSelector ':focus'
describe "when there are no columns to the right of the focused pane", ->
it "keeps the current pane focused", ->
pane6.focus()
container.focusPaneOnRight()
expect(pane6.activeItem).toMatchSelector ':focus'

View File

@@ -2,6 +2,7 @@ Delegator = require 'delegato'
{$, View} = require './space-pen-extensions'
PaneView = require './pane-view'
PaneContainer = require './pane-container'
PositionallyAwarePane = require './positionally-aware-pane'
# Private: Manages the list of panes within a {WorkspaceView}
module.exports =
@@ -98,3 +99,18 @@ class PaneContainerView extends View
focusPreviousPane: ->
@model.activatePreviousPane()
focusPaneAbove: ->
@positionallyAwarePaneForActivePane().focusPaneAbove()
focusPaneBelow: ->
@positionallyAwarePaneForActivePane().focusPaneBelow()
focusPaneOnLeft: ->
@positionallyAwarePaneForActivePane().focusPaneOnLeft()
focusPaneOnRight: ->
@positionallyAwarePaneForActivePane().focusPaneOnRight()
positionallyAwarePaneForActivePane: ->
new PositionallyAwarePane(@getActivePane(), @getPanes())

View File

@@ -0,0 +1,194 @@
_ = require 'underscore-plus'
# Private: Wraps a {Pane} to decorate it with knowledge of its physicial
# location relative to all other {Pane}s.
#
# Intended as a helper for {PaneContainerView}.
module.exports =
class PositionallyAwarePane
# Creates a {PositionallyAwarePane}.
#
# * pane:
# The {Pane} that needs to gain some positional awareness.
# * allPanes:
# The collection of all {Pane}s.
constructor: (@pane, @allPanes) ->
focusPaneAbove: ->
@bestChoiceForVerticalNavigation(@panesInAdjecentRowAbove())?.focus()
focusPaneBelow: ->
@bestChoiceForVerticalNavigation(@panesInAdjecentRowBelow())?.focus()
focusPaneOnLeft: ->
@bestChoiceForHorizontalNavigation(@panesInAdjecentColumnOnLeft())?.focus()
focusPaneOnRight: ->
@bestChoiceForHorizontalNavigation(@panesInAdjecentColumnOnRight())?.focus()
focus: ->
@pane.focus()
width: ->
@pane.width()
height: ->
@pane.height()
xLeft: ->
@pane.offset().left
xCenter: ->
@xLeft() + @width()/2
xRight: ->
@xLeft() + @width()
yTop: ->
@pane.offset().top
yCenter: ->
@yTop() + @height()/2
yBottom: ->
@yTop() + @height()
### Internal ###
panesInAdjecentRowAbove: ->
allPanesAbove = @otherPanes().filter (pane) => @isBelow(pane)
yBottomValues = _.map allPanesAbove, (pane) -> pane.yBottom()
maxYBottom = _.max yBottomValues
panesVerticallyNearest = allPanesAbove.filter (pane) ->
pane.yBottom() == maxYBottom
panesInAdjecentRowBelow: ->
allPanesBelow = @otherPanes().filter (pane) => @isAbove(pane)
yTopValues = _.map allPanesBelow, (pane) -> pane.yTop()
minYTop = _.min yTopValues
panesVerticallyNearest = allPanesBelow.filter (pane) ->
pane.yTop() == minYTop
panesInAdjecentColumnOnLeft: ->
allPanesOnLeft = @otherPanes().filter (pane) => @isRightOf(pane)
xRightValues = _.map allPanesOnLeft, (pane) -> pane.xRight()
maxXRight = _.max xRightValues
panesHorizontallyNearest = allPanesOnLeft.filter (pane) ->
pane.xRight() == maxXRight
# Internal
panesInAdjecentColumnOnRight: ->
allPanesOnRight = @otherPanes().filter (pane) => @isLeftOf(pane)
xLeftValues = _.map allPanesOnRight, (pane) -> pane.xLeft()
minXLeft = _.min xLeftValues
panesHorizontallyNearest = allPanesOnRight.filter (pane) ->
pane.xLeft() == minXLeft
# Determine whether this pane is above the given pane.
#
# * otherPane:
# The {PositionallyAwarePane} to compare to this pane.
#
# Returns true if this pane is above otherPane; otherwise, false.
isAbove: (otherPane) ->
otherPaneYTop = otherPane.yTop() + @overlap()
otherPaneYTop >= @yBottom()
# Determine whether this pane is below the given pane.
#
# * otherPane:
# The {PositionallyAwarePane} to compare to this pane.
#
# Returns true if this pane is below otherPane; otherwise, false.
isBelow: (otherPane) ->
otherPaneYBottom = otherPane.yBottom() - @overlap()
otherPaneYBottom <= @yTop()
# Determine whether this pane is to the left of the given pane.
#
# * otherPane:
# The {PositionallyAwarePane} to compare to this pane.
#
# Returns true if this pane is to the left of otherPane; otherwise, false.
isLeftOf: (otherPane) ->
otherPaneXLeft = otherPane.xLeft() + @overlap()
otherPaneXLeft >= @xRight()
# Determine whether this pane is to the right of the given pane.
#
# * otherPane:
# The {PositionallyAwarePane} to compare to this pane.
#
# Returns true if this pane is to the right of otherPane; otherwise, false.
isRightOf: (otherPane) ->
otherPaneXRight = otherPane.xRight() - @overlap()
otherPaneXRight <= @xLeft()
# The adjacent column may include several panes. When navigating left or right
# from this pane, find the pane in the adjacent column that is the most
# appropriate destination.
#
# * panes:
# An Array of {PositionallyAwarePane}s in the column adjacent to this pane.
#
# Returns a PositionallyAwarePane.
bestChoiceForHorizontalNavigation: (panes) ->
_.find panes, (pane) =>
pane.yTop() <= @yCenter() and @yCenter() <= pane.yBottom()
# The adjacent row may include several panes. When navigating up or down from
# this pane, find the pane in the adjacent row that is the most appropriate
# destination.
#
# * panes:
# An Array of {PositionallyAwarePane}s in the row adjacent to this pane.
#
# Returns a PositionallyAwarePane.
bestChoiceForVerticalNavigation: (panes) ->
_.find panes, (pane) =>
pane.xLeft() <= @xCenter() and @xCenter() <= pane.xRight()
# In theory, if two panes are side-by-side, then the rightmost x coordinate of
# the pane on the left should be less than or equal to the leftmost x
# coordinate of the pane on the right. For example, assume we have two panes:
#
# -----
# |1|2|
# -----
#
# If the rightmost x coordinate of Pane #1 is 400, then the leftmost x
# coordinate of Pane #2 should be at least 400. In practice, this isn't always
# true. Sometimes there seems to be a small "overlap" between the two panes.
# If Pane #1's rightmost x coordinate is 400, then Pane 2's leftmost x
# coordinate might be 399.2 (for example).
#
# A similar issue occurs for the y coordinates.
#
# To cope with this issue, this method provides a rough guess as to the
# amount of overlap between panes.
#
# Returns a Number.
overlap: ->
2
# Returns an Array of {PositionallyAwarePane}s for all of the other panes,
# excluding this pane.
otherPanes: ->
_.map @allPanes, (pane) -> new PositionallyAwarePane(pane, @allPanes)
coordinates: ->
xLeft: @xLeft()
xCenter: @xCenter()
xRight: @xRight()
yTop: @yTop()
yCenter: @yCenter()
yBottom: @yBottom()
logDebugInfo: ->
console.log "Coordinates for this pane:"
console.log @coordinates()
console.log "Coordinates for other panes:"
@otherPanes().forEach (pane) -> console.log pane.coordinates()

View File

@@ -121,6 +121,10 @@ class WorkspaceView extends View
@command 'window:focus-next-pane', => @focusNextPane()
@command 'window:focus-previous-pane', => @focusPreviousPane()
@command 'window:focus-pane-above', => @focusPaneAbove()
@command 'window:focus-pane-below', => @focusPaneBelow()
@command 'window:focus-pane-on-left', => @focusPaneOnLeft()
@command 'window:focus-pane-on-right', => @focusPaneOnRight()
@command 'window:save-all', => @saveAll()
@command 'window:toggle-invisibles', =>
atom.config.toggle("editor.showInvisibles")
@@ -245,6 +249,19 @@ class WorkspaceView extends View
# Public: Focuses the next pane by id.
focusNextPane: -> @model.activateNextPane()
# Public: Focuses the pane directly above the currently-focused pane.
focusPaneAbove: -> @panes.focusPaneAbove()
# Public: Focuses the pane directly below the currently-focused pane.
focusPaneBelow: -> @panes.focusPaneBelow()
# Public: Focuses the pane directly to the left of the currently-focused pane.
focusPaneOnLeft: -> @panes.focusPaneOnLeft()
# Public: Focuses the pane directly to the right of the currently-focused
# pane.
focusPaneOnRight: -> @panes.focusPaneOnRight()
# Public:
#
# FIXME: Difference between active and focused pane?