mirror of
https://github.com/atom/atom.git
synced 2026-01-24 14:28:14 -05:00
This causes the active item to change in the model before the associated view can be added, which causes problems with the ReactEditorView having methods called on it before its afterAttach hook creates the component. We call Pane::activate subsequently unless activation is suppressed, which will focus the pane anyway, so this was redundant.
368 lines
11 KiB
CoffeeScript
368 lines
11 KiB
CoffeeScript
{find, compact, extend, last} = require 'underscore-plus'
|
|
{Model, Sequence} = require 'theorist'
|
|
Serializable = require 'serializable'
|
|
PaneAxis = require './pane-axis'
|
|
Editor = require './editor'
|
|
PaneView = null
|
|
|
|
# Public: A container for multiple items, one of which is *active* at a given
|
|
# time. With the default packages, a tab is displayed for each item and the
|
|
# active item's view is displayed.
|
|
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
|
|
|
|
@items = Sequence.fromArray(compact(params?.items ? []))
|
|
@activeItem ?= @items[0]
|
|
|
|
@subscribe @items.onEach (item) =>
|
|
if typeof item.on is 'function'
|
|
@subscribe item, 'destroyed', => @removeItem(item, true)
|
|
|
|
@subscribe @items.onRemoval (item, index) =>
|
|
@unsubscribe item if typeof item.on is 'function'
|
|
|
|
@activate() if params?.active
|
|
|
|
# Called by the Serializable mixin during serialization.
|
|
serializeParams: ->
|
|
items: compact(@items.map((item) -> item.serialize?()))
|
|
activeItemUri: @activeItem?.getUri?()
|
|
focused: @focused
|
|
active: @active
|
|
|
|
# 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'
|
|
|
|
isActive: -> @active
|
|
|
|
# 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
|
|
|
|
# Public: Makes this pane the *active* pane, causing it to gain focus
|
|
# immediately.
|
|
activate: ->
|
|
@container?.activePane = this
|
|
@emit 'activated'
|
|
|
|
getPanes: -> [this]
|
|
|
|
# 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
|
|
|
|
# Public: Returns an {Editor} if the pane item is an {Editor}, or null
|
|
# otherwise.
|
|
getActiveEditor: ->
|
|
@activeItem if @activeItem instanceof Editor
|
|
|
|
# Public: Returns the item at the specified 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)
|
|
|
|
# Returns the index of the current active item.
|
|
getActiveItemIndex: ->
|
|
@items.indexOf(@activeItem)
|
|
|
|
# Makes the item at the given index active.
|
|
activateItemAtIndex: (index) ->
|
|
@activateItem(@itemAtIndex(index))
|
|
|
|
# Makes the given item active, adding the item if necessary.
|
|
activateItem: (item) ->
|
|
if item?
|
|
@addItem(item)
|
|
@activeItem = item
|
|
|
|
# Public: Adds the item to the pane.
|
|
#
|
|
# item - The item to add. It can be a model with an associated view or a view.
|
|
# index - An optional 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
|
|
|
|
@items.splice(index, 0, item)
|
|
@emit 'item-added', item, index
|
|
@activeItem ?= item
|
|
item
|
|
|
|
# Public: Adds the given items to the pane.
|
|
#
|
|
# items - An {Array} of items to add. Items can be models with associated
|
|
# views or views. Any items that are already present in items will
|
|
# not be added.
|
|
# index - An optional index at which to add the item. If omitted, the item is
|
|
# added after the current active item.
|
|
#
|
|
# Returns an {Array} of the 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, destroying) ->
|
|
index = @items.indexOf(item)
|
|
return if index is -1
|
|
if item is @activeItem
|
|
if @items.length is 1
|
|
@activeItem = undefined
|
|
else if index is 0
|
|
@activateNextItem()
|
|
else
|
|
@activatePreviousItem()
|
|
@items.splice(index, 1)
|
|
@emit 'item-removed', item, index, destroying
|
|
@container?.itemDestroyed(item) if destroying
|
|
@destroy() if @items.length is 0 and atom.config.get('core.destroyEmptyPanes')
|
|
|
|
# Public: Moves the given item to the specified index.
|
|
moveItem: (item, newIndex) ->
|
|
oldIndex = @items.indexOf(item)
|
|
@items.splice(oldIndex, 1)
|
|
@items.splice(newIndex, 0, item)
|
|
@emit 'item-moved', item, newIndex
|
|
|
|
# Public: Moves the given item to the given index at another pane.
|
|
moveItemToPane: (item, pane, index) ->
|
|
pane.addItem(item, index)
|
|
@removeItem(item)
|
|
|
|
# Public: Destroys the currently active item and make the next item active.
|
|
destroyActiveItem: ->
|
|
@destroyItem(@activeItem)
|
|
false
|
|
|
|
# Public: Destroys the given item. If it is the active item, activate the next
|
|
# one. If this is the last item, also destroys the pane.
|
|
destroyItem: (item) ->
|
|
if item?
|
|
@emit 'before-item-destroyed', item
|
|
if @promptToSaveItem(item)
|
|
@removeItem(item, true)
|
|
item.destroy?()
|
|
true
|
|
else
|
|
false
|
|
|
|
# Public: Destroys all items and destroys the pane.
|
|
destroyItems: ->
|
|
@destroyItem(item) for item in @getItems()
|
|
|
|
# Public: Destroys all items but the active one.
|
|
destroyInactiveItems: ->
|
|
@destroyItem(item) for item in @getItems() when item isnt @activeItem
|
|
|
|
destroy: ->
|
|
if @container?.isAlive() and @container.getPanes().length is 1
|
|
@destroyItems()
|
|
else
|
|
super
|
|
|
|
# Called by model superclass.
|
|
destroyed: ->
|
|
@container.activateNextPane() if @isActive()
|
|
item.destroy?() for item in @items.slice()
|
|
|
|
# Public: Prompts the user to save the given item if it can be saved and is
|
|
# currently unsaved.
|
|
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: Saves the active item.
|
|
saveActiveItem: ->
|
|
@saveItem(@activeItem)
|
|
|
|
# Public: Saves the active item at a prompted-for location.
|
|
saveActiveItemAs: ->
|
|
@saveItemAs(@activeItem)
|
|
|
|
# Public: Saves the specified item.
|
|
#
|
|
# item - The item to save.
|
|
# nextAction - An optional function which will be called after the item is
|
|
# saved.
|
|
saveItem: (item, nextAction) ->
|
|
if item?.getUri?()
|
|
item.save?()
|
|
nextAction?()
|
|
else
|
|
@saveItemAs(item, nextAction)
|
|
|
|
# Public: Saves the given item at a prompted-for location.
|
|
#
|
|
# item - The item to save.
|
|
# nextAction - An optional function which will be called after the item is
|
|
# saved.
|
|
saveItemAs: (item, nextAction) ->
|
|
return unless item?.saveAs?
|
|
|
|
itemPath = item.getPath?()
|
|
newItemPath = atom.showSaveDialogSync(itemPath)
|
|
if newItemPath
|
|
item.saveAs(newItemPath)
|
|
nextAction?()
|
|
|
|
# Public: Saves all items.
|
|
saveItems: ->
|
|
@saveItem(item) for item in @getItems()
|
|
|
|
# Public: Returns the first item that matches the given URI or undefined if
|
|
# none exists.
|
|
itemForUri: (uri) ->
|
|
find @items, (item) -> item.getUri?() is uri
|
|
|
|
# Public: Activates the first item that matches the given URI. Returns a
|
|
# boolean indicating whether a matching item was found.
|
|
activateItemForUri: (uri) ->
|
|
if item = @itemForUri(uri)
|
|
@activateItem(item)
|
|
true
|
|
else
|
|
false
|
|
|
|
copyActiveItem: ->
|
|
if @activeItem?
|
|
@activeItem.copy?() ? atom.deserializers.deserialize(@activeItem.serialize())
|
|
|
|
# Public: Creates a new pane to the left of the receiver.
|
|
#
|
|
# params - An object with keys:
|
|
# :items - An optional array of items with which to construct the new pane.
|
|
#
|
|
# Returns the new {Pane}.
|
|
splitLeft: (params) ->
|
|
@split('horizontal', 'before', params)
|
|
|
|
# Public: Creates a new pane to the right of the receiver.
|
|
#
|
|
# params - An object with keys:
|
|
# :items - An optional array of items with which to construct the new pane.
|
|
#
|
|
# Returns the new {Pane}.
|
|
splitRight: (params) ->
|
|
@split('horizontal', 'after', params)
|
|
|
|
# Public: Creates a new pane above the receiver.
|
|
#
|
|
# params - An object with keys:
|
|
# :items - An optional array of items with which to construct the new pane.
|
|
#
|
|
# Returns the new {Pane}.
|
|
splitUp: (params) ->
|
|
@split('vertical', 'before', params)
|
|
|
|
# Public: Creates a new pane below the receiver.
|
|
#
|
|
# params - An object with keys:
|
|
# :items - An optional array of items with which to construct the new pane.
|
|
#
|
|
# Returns the new {Pane}.
|
|
splitDown: (params) ->
|
|
@split('vertical', 'after', params)
|
|
|
|
split: (orientation, side, params) ->
|
|
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()
|