mirror of
https://github.com/atom/atom.git
synced 2026-01-22 13:28:01 -05:00
610 lines
20 KiB
CoffeeScript
610 lines
20 KiB
CoffeeScript
{find, compact, extend, last} = require 'underscore-plus'
|
|
{Model} = require 'theorist'
|
|
{Emitter} = require 'event-kit'
|
|
Serializable = require 'serializable'
|
|
Grim = require 'grim'
|
|
PaneAxis = require './pane-axis'
|
|
TextEditor = require './text-editor'
|
|
PaneView = null
|
|
|
|
# Extended: A container for presenting content in the center of the workspace.
|
|
# Panes can contain multiple items, one of which is *active* at a given time.
|
|
# The view corresponding to the active item is displayed in the interface. In
|
|
# the default configuration, tabs are also displayed for each item.
|
|
module.exports =
|
|
class Pane extends Model
|
|
atom.deserializers.add(this)
|
|
Serializable.includeInto(this)
|
|
|
|
@properties
|
|
container: undefined
|
|
activeItem: undefined
|
|
focused: false
|
|
|
|
# Public: Only one pane is considered *active* at a time. A pane is activated
|
|
# when it is focused, and when focus returns to the pane container after
|
|
# moving to another element such as a panel, it returns to the active pane.
|
|
@behavior 'active', ->
|
|
@$container
|
|
.switch((container) -> container?.$activePane)
|
|
.map((activePane) => activePane is this)
|
|
.distinctUntilChanged()
|
|
|
|
constructor: (params) ->
|
|
super
|
|
|
|
@emitter = new Emitter
|
|
@items = []
|
|
|
|
@addItems(compact(params?.items ? []))
|
|
@setActiveItem(@items[0]) unless @getActiveItem()?
|
|
|
|
# Called by the Serializable mixin during serialization.
|
|
serializeParams: ->
|
|
id: @id
|
|
items: compact(@items.map((item) -> item.serialize?()))
|
|
activeItemUri: @activeItem?.getUri?()
|
|
focused: @focused
|
|
|
|
# Called by the Serializable mixin during deserialization.
|
|
deserializeParams: (params) ->
|
|
{items, activeItemUri} = params
|
|
params.items = compact(items.map (itemState) -> atom.deserializers.deserialize(itemState))
|
|
params.activeItem = find params.items, (item) -> item.getUri?() is activeItemUri
|
|
params
|
|
|
|
# Called by the view layer to construct a view for this model.
|
|
getViewClass: -> PaneView ?= require './pane-view'
|
|
|
|
getView: (object) ->
|
|
@container.getView(object)
|
|
|
|
getParent: -> @parent
|
|
|
|
setParent: (@parent) -> @parent
|
|
|
|
getContainer: -> @container
|
|
|
|
setContainer: (container) ->
|
|
container.didAddPane({pane: this}) unless container is @container
|
|
@container = container
|
|
|
|
###
|
|
Section: Event Subscription
|
|
###
|
|
|
|
# Public: Invoke the given callback when the pane is activated.
|
|
#
|
|
# The given callback will be invoked whenever {::activate} is called on the
|
|
# pane, even if it is already active at the time.
|
|
#
|
|
# * `callback` {Function} to be called when the pane is activated.
|
|
#
|
|
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidActivate: (callback) ->
|
|
@emitter.on 'did-activate', callback
|
|
|
|
# Public: Invoke the given callback when the pane is destroyed.
|
|
#
|
|
# * `callback` {Function} to be called when the pane is destroyed.
|
|
#
|
|
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidDestroy: (callback) ->
|
|
@emitter.on 'did-destroy', callback
|
|
|
|
# Public: Invoke the given callback when the value of the {::isActive}
|
|
# property changes.
|
|
#
|
|
# * `callback` {Function} to be called when the value of the {::isActive}
|
|
# property changes.
|
|
# * `active` {Boolean} indicating whether the pane is active.
|
|
#
|
|
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidChangeActive: (callback) ->
|
|
@container.onDidChangeActivePane (activePane) =>
|
|
callback(this is activePane)
|
|
|
|
# Public: Invoke the given callback with the current and future values of the
|
|
# {::isActive} property.
|
|
#
|
|
# * `callback` {Function} to be called with the current and future values of
|
|
# the {::isActive} property.
|
|
# * `active` {Boolean} indicating whether the pane is active.
|
|
#
|
|
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
observeActive: (callback) ->
|
|
callback(@isActive())
|
|
@onDidChangeActive(callback)
|
|
|
|
# Public: Invoke the given callback when an item is added to the pane.
|
|
#
|
|
# * `callback` {Function} to be called with when items are added.
|
|
# * `event` {Object} with the following keys:
|
|
# * `item` The added pane item.
|
|
# * `index` {Number} indicating where the item is located.
|
|
#
|
|
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidAddItem: (callback) ->
|
|
@emitter.on 'did-add-item', callback
|
|
|
|
# Public: Invoke the given callback when an item is removed from the pane.
|
|
#
|
|
# * `callback` {Function} to be called with when items are removed.
|
|
# * `event` {Object} with the following keys:
|
|
# * `item` The removed pane item.
|
|
# * `index` {Number} indicating where the item was located.
|
|
#
|
|
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidRemoveItem: (callback) ->
|
|
@emitter.on 'did-remove-item', callback
|
|
|
|
# Public: Invoke the given callback when an item is moved within the pane.
|
|
#
|
|
# * `callback` {Function} to be called with when items are moved.
|
|
# * `event` {Object} with the following keys:
|
|
# * `item` The removed pane item.
|
|
# * `oldIndex` {Number} indicating where the item was located.
|
|
# * `newIndex` {Number} indicating where the item is now located.
|
|
#
|
|
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidMoveItem: (callback) ->
|
|
@emitter.on 'did-move-item', callback
|
|
|
|
# Public: Invoke the given callback with all current and future items.
|
|
#
|
|
# * `callback` {Function} to be called with current and future items.
|
|
# * `item` An item that is present in {::getItems} at the time of
|
|
# subscription or that is added at some later time.
|
|
#
|
|
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
observeItems: (callback) ->
|
|
callback(item) for item in @getItems()
|
|
@onDidAddItem ({item}) -> callback(item)
|
|
|
|
# Public: Invoke the given callback when the value of {::getActiveItem}
|
|
# changes.
|
|
#
|
|
# * `callback` {Function} to be called with when the active item changes.
|
|
# * `activeItem` The current active item.
|
|
#
|
|
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
onDidChangeActiveItem: (callback) ->
|
|
@emitter.on 'did-change-active-item', callback
|
|
|
|
# Public: Invoke the given callback with the current and future values of
|
|
# {::getActiveItem}.
|
|
#
|
|
# * `callback` {Function} to be called with the current and future active
|
|
# items.
|
|
# * `activeItem` The current active item.
|
|
#
|
|
# Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
|
|
observeActiveItem: (callback) ->
|
|
callback(@getActiveItem())
|
|
@onDidChangeActiveItem(callback)
|
|
|
|
# Public: Invoke the given callback before items are destroyed.
|
|
#
|
|
# * `callback` {Function} to be called before items are destroyed.
|
|
# * `event` {Object} with the following keys:
|
|
# * `item` The item that will be destroyed.
|
|
# * `index` The location of the item.
|
|
#
|
|
# Returns a {Disposable} on which `.dispose()` can be called to
|
|
# unsubscribe.
|
|
onWillDestroyItem: (callback) ->
|
|
@emitter.on 'will-destroy-item', callback
|
|
|
|
on: (eventName) ->
|
|
switch eventName
|
|
when 'activated'
|
|
Grim.deprecate("Use Pane::onDidActivate instead")
|
|
when 'destroyed'
|
|
Grim.deprecate("Use Pane::onDidDestroy instead")
|
|
when 'item-added'
|
|
Grim.deprecate("Use Pane::onDidAddItem instead")
|
|
when 'item-removed'
|
|
Grim.deprecate("Use Pane::onDidRemoveItem instead")
|
|
when 'item-moved'
|
|
Grim.deprecate("Use Pane::onDidMoveItem instead")
|
|
when 'before-item-destroyed'
|
|
Grim.deprecate("Use Pane::onWillDestroyItem instead")
|
|
else
|
|
Grim.deprecate("Subscribing via ::on is deprecated. Use documented event subscription methods instead.")
|
|
super
|
|
|
|
behavior: (behaviorName) ->
|
|
switch behaviorName
|
|
when 'active'
|
|
Grim.deprecate("The $active behavior property is deprecated. Use ::observeActive or ::onDidChangeActive instead.")
|
|
when 'container'
|
|
Grim.deprecate("The $container behavior property is deprecated.")
|
|
when 'activeItem'
|
|
Grim.deprecate("The $activeItem behavior property is deprecated. Use ::observeActiveItem or ::onDidChangeActiveItem instead.")
|
|
when 'focused'
|
|
Grim.deprecate("The $focused behavior property is deprecated.")
|
|
else
|
|
Grim.deprecate("Pane::behavior is deprecated. Use event subscription methods instead.")
|
|
|
|
super
|
|
|
|
# Called by the view layer to indicate that the pane has gained focus.
|
|
focus: ->
|
|
@focused = true
|
|
@activate() unless @isActive()
|
|
|
|
# Called by the view layer to indicate that the pane has lost focus.
|
|
blur: ->
|
|
@focused = false
|
|
true # if this is called from an event handler, don't cancel it
|
|
|
|
getPanes: -> [this]
|
|
|
|
###
|
|
Section: Items
|
|
###
|
|
|
|
# Public: Get the items in this pane.
|
|
#
|
|
# Returns an {Array} of items.
|
|
getItems: ->
|
|
@items.slice()
|
|
|
|
# Public: Get the active pane item in this pane.
|
|
#
|
|
# Returns a pane item.
|
|
getActiveItem: -> @activeItem
|
|
|
|
setActiveItem: (activeItem) ->
|
|
unless activeItem is @activeItem
|
|
@activeItem = activeItem
|
|
@emitter.emit 'did-change-active-item', @activeItem
|
|
@activeItem
|
|
|
|
# Return an {TextEditor} if the pane item is an {TextEditor}, or null otherwise.
|
|
getActiveEditor: ->
|
|
@activeItem if @activeItem instanceof TextEditor
|
|
|
|
# Public: Return the item at the given index.
|
|
#
|
|
# * `index` {Number}
|
|
#
|
|
# Returns an item or `null` if no item exists at the given index.
|
|
itemAtIndex: (index) ->
|
|
@items[index]
|
|
|
|
# Public: Makes the next item active.
|
|
activateNextItem: ->
|
|
index = @getActiveItemIndex()
|
|
if index < @items.length - 1
|
|
@activateItemAtIndex(index + 1)
|
|
else
|
|
@activateItemAtIndex(0)
|
|
|
|
# Public: Makes the previous item active.
|
|
activatePreviousItem: ->
|
|
index = @getActiveItemIndex()
|
|
if index > 0
|
|
@activateItemAtIndex(index - 1)
|
|
else
|
|
@activateItemAtIndex(@items.length - 1)
|
|
|
|
# Public: Get the index of the active item.
|
|
#
|
|
# Returns a {Number}.
|
|
getActiveItemIndex: ->
|
|
@items.indexOf(@activeItem)
|
|
|
|
# Public: Activate the item at the given index.
|
|
#
|
|
# * `index` {Number}
|
|
activateItemAtIndex: (index) ->
|
|
@activateItem(@itemAtIndex(index))
|
|
|
|
# Public: Make the given item *active*, causing it to be displayed by
|
|
# the pane's view.
|
|
activateItem: (item) ->
|
|
if item?
|
|
@addItem(item)
|
|
@setActiveItem(item)
|
|
|
|
# Public: Add the given item to the pane.
|
|
#
|
|
# * `item` The item to add. It can be a model with an associated view or a
|
|
# view.
|
|
# * `index` (optional) {Number} indicating the index at which to add the item.
|
|
# If omitted, the item is added after the current active item.
|
|
#
|
|
# Returns the added item.
|
|
addItem: (item, index=@getActiveItemIndex() + 1) ->
|
|
return if item in @items
|
|
|
|
if typeof item.on is 'function'
|
|
@subscribe item, 'destroyed', => @removeItem(item, true)
|
|
|
|
@items.splice(index, 0, item)
|
|
@emit 'item-added', item, index
|
|
@emitter.emit 'did-add-item', {item, index}
|
|
@setActiveItem(item) unless @getActiveItem()?
|
|
item
|
|
|
|
# Public: Add the given items to the pane.
|
|
#
|
|
# * `items` An {Array} of items to add. Items can be views or models with
|
|
# associated views. Any objects that are already present in the pane's
|
|
# current items will not be added again.
|
|
# * `index` (optional) {Number} index at which to add the items. If omitted,
|
|
# the item is # added after the current active item.
|
|
#
|
|
# Returns an {Array} of added items.
|
|
addItems: (items, index=@getActiveItemIndex() + 1) ->
|
|
items = items.filter (item) => not (item in @items)
|
|
@addItem(item, index + i) for item, i in items
|
|
items
|
|
|
|
removeItem: (item, destroyed=false) ->
|
|
index = @items.indexOf(item)
|
|
return if index is -1
|
|
|
|
if typeof item.on is 'function'
|
|
@unsubscribe item
|
|
|
|
if item is @activeItem
|
|
if @items.length is 1
|
|
@setActiveItem(undefined)
|
|
else if index is 0
|
|
@activateNextItem()
|
|
else
|
|
@activatePreviousItem()
|
|
@items.splice(index, 1)
|
|
@emit 'item-removed', item, index, destroyed
|
|
@emitter.emit 'did-remove-item', {item, index, destroyed}
|
|
@container?.paneItemDestroyed(item) if destroyed
|
|
@destroy() if @items.length is 0 and atom.config.get('core.destroyEmptyPanes')
|
|
|
|
# Public: Move the given item to the given index.
|
|
#
|
|
# * `item` The item to move.
|
|
# * `index` {Number} indicating the index to which to move the item.
|
|
moveItem: (item, newIndex) ->
|
|
oldIndex = @items.indexOf(item)
|
|
@items.splice(oldIndex, 1)
|
|
@items.splice(newIndex, 0, item)
|
|
@emit 'item-moved', item, newIndex
|
|
@emitter.emit 'did-move-item', {item, oldIndex, newIndex}
|
|
|
|
# Public: Move the given item to the given index on another pane.
|
|
#
|
|
# * `item` The item to move.
|
|
# * `pane` {Pane} to which to move the item.
|
|
# * `index` {Number} indicating the index to which to move the item in the
|
|
# given pane.
|
|
moveItemToPane: (item, pane, index) ->
|
|
@removeItem(item)
|
|
pane.addItem(item, index)
|
|
|
|
# Public: Destroy the active item and activate the next item.
|
|
destroyActiveItem: ->
|
|
@destroyItem(@activeItem)
|
|
false
|
|
|
|
# Public: Destroy the given item.
|
|
#
|
|
# If the item is active, the next item will be activated. If the item is the
|
|
# last item, the pane will be destroyed if the `core.destroyEmptyPanes` config
|
|
# setting is `true`.
|
|
destroyItem: (item) ->
|
|
index = @items.indexOf(item)
|
|
if index isnt -1
|
|
@emit 'before-item-destroyed', item
|
|
@emitter.emit 'will-destroy-item', {item, index}
|
|
if @promptToSaveItem(item)
|
|
@removeItem(item, true)
|
|
item.destroy?()
|
|
true
|
|
else
|
|
false
|
|
|
|
# Public: Destroy all items.
|
|
destroyItems: ->
|
|
@destroyItem(item) for item in @getItems()
|
|
|
|
# Public: Destroy all items except for the active item.
|
|
destroyInactiveItems: ->
|
|
@destroyItem(item) for item in @getItems() when item isnt @activeItem
|
|
|
|
promptToSaveItem: (item) ->
|
|
return true unless item.shouldPromptToSave?()
|
|
|
|
uri = item.getUri()
|
|
chosen = atom.confirm
|
|
message: "'#{item.getTitle?() ? item.getUri()}' has changes, do you want to save them?"
|
|
detailedMessage: "Your changes will be lost if you close this item without saving."
|
|
buttons: ["Save", "Cancel", "Don't Save"]
|
|
|
|
switch chosen
|
|
when 0 then @saveItem(item, -> true)
|
|
when 1 then false
|
|
when 2 then true
|
|
|
|
# Public: Save the active item.
|
|
saveActiveItem: (nextAction) ->
|
|
@saveItem(@getActiveItem(), nextAction)
|
|
|
|
# Public: Prompt the user for a location and save the active item with the
|
|
# path they select.
|
|
#
|
|
# * `nextAction` (optional) {Function} which will be called after the item is
|
|
# successfully saved.
|
|
saveActiveItemAs: (nextAction) ->
|
|
@saveItemAs(@getActiveItem(), nextAction)
|
|
|
|
# Public: Save the given item.
|
|
#
|
|
# * `item` The item to save.
|
|
# * `nextAction` (optional) {Function} which will be called after the item is
|
|
# successfully saved.
|
|
saveItem: (item, nextAction) ->
|
|
if item?.getUri?()
|
|
item.save?()
|
|
nextAction?()
|
|
else
|
|
@saveItemAs(item, nextAction)
|
|
|
|
# Public: Prompt the user for a location and save the active item with the
|
|
# path they select.
|
|
#
|
|
# * `item` The item to save.
|
|
# * `nextAction` (optional) {Function} which will be called after the item is
|
|
# successfully saved.
|
|
saveItemAs: (item, nextAction) ->
|
|
return unless item?.saveAs?
|
|
|
|
itemPath = item.getPath?()
|
|
newItemPath = atom.showSaveDialogSync(itemPath)
|
|
if newItemPath
|
|
item.saveAs(newItemPath)
|
|
nextAction?()
|
|
|
|
# Public: Save all items.
|
|
saveItems: ->
|
|
@saveItem(item) for item in @getItems()
|
|
|
|
# Public: Return the first item that matches the given URI or undefined if
|
|
# none exists.
|
|
#
|
|
# * `uri` {String} containing a URI.
|
|
itemForUri: (uri) ->
|
|
find @items, (item) -> item.getUri?() is uri
|
|
|
|
# Public: Activate the first item that matches the given URI.
|
|
#
|
|
# Returns a {Boolean} indicating whether an item matching the URI was found.
|
|
activateItemForUri: (uri) ->
|
|
if item = @itemForUri(uri)
|
|
@activateItem(item)
|
|
true
|
|
else
|
|
false
|
|
|
|
copyActiveItem: ->
|
|
if @activeItem?
|
|
@activeItem.copy?() ? atom.deserializers.deserialize(@activeItem.serialize())
|
|
|
|
###
|
|
Section: Lifecycle
|
|
###
|
|
|
|
# Public: Determine whether the pane is active.
|
|
#
|
|
# Returns a {Boolean}.
|
|
isActive: ->
|
|
@container?.getActivePane() is this
|
|
|
|
# Public: Makes this pane the *active* pane, causing it to gain focus.
|
|
activate: ->
|
|
@container?.setActivePane(this)
|
|
@emit 'activated'
|
|
@emitter.emit 'did-activate'
|
|
|
|
# Public: Close the pane and destroy all its items.
|
|
#
|
|
# If this is the last pane, all the items will be destroyed but the pane
|
|
# itself will not be destroyed.
|
|
destroy: ->
|
|
if @container?.isAlive() and @container.getPanes().length is 1
|
|
@destroyItems()
|
|
else
|
|
super
|
|
|
|
# Called by model superclass.
|
|
destroyed: ->
|
|
@container.activateNextPane() if @isActive()
|
|
@emitter.emit 'did-destroy'
|
|
@emitter.dispose()
|
|
item.destroy?() for item in @items.slice()
|
|
|
|
###
|
|
Section: Splitting
|
|
###
|
|
|
|
# Public: Create a new pane to the left of this pane.
|
|
#
|
|
# * `params` (optional) {Object} with the following keys:
|
|
# * `items` (optional) {Array} of items to add to the new pane.
|
|
# * `copyActiveItem` (optional) {Boolean} true will copy the active item into the new split pane
|
|
#
|
|
# Returns the new {Pane}.
|
|
splitLeft: (params) ->
|
|
@split('horizontal', 'before', params)
|
|
|
|
# Public: Create a new pane to the right of this pane.
|
|
#
|
|
# * `params` (optional) {Object} with the following keys:
|
|
# * `items` (optional) {Array} of items to add to the new pane.
|
|
# * `copyActiveItem` (optional) {Boolean} true will copy the active item into the new split pane
|
|
#
|
|
# Returns the new {Pane}.
|
|
splitRight: (params) ->
|
|
@split('horizontal', 'after', params)
|
|
|
|
# Public: Creates a new pane above the receiver.
|
|
#
|
|
# * `params` (optional) {Object} with the following keys:
|
|
# * `items` (optional) {Array} of items to add to the new pane.
|
|
# * `copyActiveItem` (optional) {Boolean} true will copy the active item into the new split pane
|
|
#
|
|
# Returns the new {Pane}.
|
|
splitUp: (params) ->
|
|
@split('vertical', 'before', params)
|
|
|
|
# Public: Creates a new pane below the receiver.
|
|
#
|
|
# * `params` (optional) {Object} with the following keys:
|
|
# * `items` (optional) {Array} of items to add to the new pane.
|
|
# * `copyActiveItem` (optional) {Boolean} true will copy the active item into the new split pane
|
|
#
|
|
# Returns the new {Pane}.
|
|
splitDown: (params) ->
|
|
@split('vertical', 'after', params)
|
|
|
|
split: (orientation, side, params) ->
|
|
if params?.copyActiveItem
|
|
params.items ?= []
|
|
params.items.push(@copyActiveItem())
|
|
|
|
if @parent.orientation isnt orientation
|
|
@parent.replaceChild(this, new PaneAxis({@container, orientation, children: [this]}))
|
|
|
|
newPane = new @constructor(params)
|
|
switch side
|
|
when 'before' then @parent.insertChildBefore(this, newPane)
|
|
when 'after' then @parent.insertChildAfter(this, newPane)
|
|
|
|
newPane.activate()
|
|
newPane
|
|
|
|
# If the parent is a horizontal axis, returns its first child if it is a pane;
|
|
# otherwise returns this pane.
|
|
findLeftmostSibling: ->
|
|
if @parent.orientation is 'horizontal'
|
|
[leftmostSibling] = @parent.children
|
|
if leftmostSibling instanceof PaneAxis
|
|
this
|
|
else
|
|
leftmostSibling
|
|
else
|
|
this
|
|
|
|
# If the parent is a horizontal axis, returns its last child if it is a pane;
|
|
# otherwise returns a new pane created by splitting this pane rightward.
|
|
findOrCreateRightmostSibling: ->
|
|
if @parent.orientation is 'horizontal'
|
|
rightmostSibling = last(@parent.children)
|
|
if rightmostSibling instanceof PaneAxis
|
|
@splitRight()
|
|
else
|
|
rightmostSibling
|
|
else
|
|
@splitRight()
|