mirror of
https://github.com/atom/atom.git
synced 2026-04-06 03:02:13 -04:00
Add commands to move directionally between panes
This commit is contained in:
@@ -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'
|
||||
|
||||
@@ -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())
|
||||
|
||||
194
src/positionally-aware-pane.coffee
Normal file
194
src/positionally-aware-pane.coffee
Normal 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()
|
||||
@@ -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?
|
||||
|
||||
Reference in New Issue
Block a user