{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: -> super unless @container?.isAlive() and @container?.getPanes().length is 1 # 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(extend({focused: true}, 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; # otherwise this pane. findLeftmostSibling: -> if @parent.orientation is 'horizontal' @parent.children[0] else this # If the parent is a horizontal axis, returns its last child; # otherwise returns a new pane created by splitting this pane rightward. findOrCreateRightmostSibling: -> if @parent.orientation is 'horizontal' last(@parent.children) else @splitRight()