Files
atom/src/pane-container.js
David Wilson 4a151ac210 Fix #14716: Error when deserializing TreeView for project
This change fixes an issue that appears when the user removes the last
project from the workspace and then re-adds it.  At this time an error
is thrown from within ItemRegistry because the PaneContainer that
holds the TreeView in the left-most dock does not clear out its
existing ItemRegistry before deserializing the old TreeView state.

The fix is to create a new ItemRegistry when a PaneContainer is
deserialized so that the previous Pane's items are not retained.
2018-01-22 15:13:37 -08:00

300 lines
8.2 KiB
JavaScript

const {find} = require('underscore-plus')
const {Emitter, CompositeDisposable} = require('event-kit')
const Pane = require('./pane')
const ItemRegistry = require('./item-registry')
const PaneContainerElement = require('./pane-container-element')
const SERIALIZATION_VERSION = 1
const STOPPED_CHANGING_ACTIVE_PANE_ITEM_DELAY = 100
module.exports =
class PaneContainer {
constructor (params) {
let applicationDelegate, deserializerManager, notificationManager;
({config: this.config, applicationDelegate, notificationManager, deserializerManager, viewRegistry: this.viewRegistry, location: this.location} = params)
this.emitter = new Emitter()
this.subscriptions = new CompositeDisposable()
this.itemRegistry = new ItemRegistry()
this.alive = true
this.stoppedChangingActivePaneItemTimeout = null
this.setRoot(new Pane({container: this, config: this.config, applicationDelegate, notificationManager, deserializerManager, viewRegistry: this.viewRegistry}))
this.didActivatePane(this.getRoot())
}
getLocation () { return this.location }
getElement () {
return this.element != null ? this.element : (this.element = new PaneContainerElement().initialize(this, {views: this.viewRegistry}))
}
destroy () {
this.alive = false
for (let pane of this.getRoot().getPanes()) { pane.destroy() }
this.cancelStoppedChangingActivePaneItemTimeout()
this.subscriptions.dispose()
this.emitter.dispose()
}
isAlive () { return this.alive }
isDestroyed () { return !this.isAlive() }
serialize (params) {
return {
deserializer: 'PaneContainer',
version: SERIALIZATION_VERSION,
root: this.root ? this.root.serialize() : null,
activePaneId: this.activePane.id
}
}
deserialize (state, deserializerManager) {
if (state.version !== SERIALIZATION_VERSION) return
this.itemRegistry = new ItemRegistry()
this.setRoot(deserializerManager.deserialize(state.root))
this.activePane = find(this.getRoot().getPanes(), pane => pane.id === state.activePaneId) || this.getPanes()[0]
if (this.config.get('core.destroyEmptyPanes')) this.destroyEmptyPanes()
}
onDidChangeRoot (fn) {
return this.emitter.on('did-change-root', fn)
}
observeRoot (fn) {
fn(this.getRoot())
return this.onDidChangeRoot(fn)
}
onDidAddPane (fn) {
return this.emitter.on('did-add-pane', fn)
}
observePanes (fn) {
for (let pane of this.getPanes()) { fn(pane) }
return this.onDidAddPane(({pane}) => fn(pane))
}
onDidDestroyPane (fn) {
return this.emitter.on('did-destroy-pane', fn)
}
onWillDestroyPane (fn) {
return this.emitter.on('will-destroy-pane', fn)
}
onDidChangeActivePane (fn) {
return this.emitter.on('did-change-active-pane', fn)
}
onDidActivatePane (fn) {
return this.emitter.on('did-activate-pane', fn)
}
observeActivePane (fn) {
fn(this.getActivePane())
return this.onDidChangeActivePane(fn)
}
onDidAddPaneItem (fn) {
return this.emitter.on('did-add-pane-item', fn)
}
observePaneItems (fn) {
for (let item of this.getPaneItems()) { fn(item) }
return this.onDidAddPaneItem(({item}) => fn(item))
}
onDidChangeActivePaneItem (fn) {
return this.emitter.on('did-change-active-pane-item', fn)
}
onDidStopChangingActivePaneItem (fn) {
return this.emitter.on('did-stop-changing-active-pane-item', fn)
}
observeActivePaneItem (fn) {
fn(this.getActivePaneItem())
return this.onDidChangeActivePaneItem(fn)
}
onWillDestroyPaneItem (fn) {
return this.emitter.on('will-destroy-pane-item', fn)
}
onDidDestroyPaneItem (fn) {
return this.emitter.on('did-destroy-pane-item', fn)
}
getRoot () { return this.root }
setRoot (root) {
this.root = root
this.root.setParent(this)
this.root.setContainer(this)
this.emitter.emit('did-change-root', this.root)
if ((this.getActivePane() == null) && this.root instanceof Pane) {
this.didActivatePane(this.root)
}
}
replaceChild (oldChild, newChild) {
if (oldChild !== this.root) { throw new Error('Replacing non-existent child') }
this.setRoot(newChild)
}
getPanes () {
if (this.alive) {
return this.getRoot().getPanes()
} else {
return []
}
}
getPaneItems () {
return this.getRoot().getItems()
}
getActivePane () {
return this.activePane
}
getActivePaneItem () {
return this.getActivePane().getActiveItem()
}
paneForURI (uri) {
return find(this.getPanes(), pane => pane.itemForURI(uri) != null)
}
paneForItem (item) {
return find(this.getPanes(), pane => pane.getItems().includes(item))
}
saveAll () {
for (let pane of this.getPanes()) { pane.saveItems() }
}
confirmClose (options) {
const promises = []
for (const pane of this.getPanes()) {
for (const item of pane.getItems()) {
promises.push(pane.promptToSaveItem(item, options))
}
}
return Promise.all(promises).then((results) => !results.includes(false))
}
activateNextPane () {
const panes = this.getPanes()
if (panes.length > 1) {
const currentIndex = panes.indexOf(this.activePane)
const nextIndex = (currentIndex + 1) % panes.length
panes[nextIndex].activate()
return true
} else {
return false
}
}
activatePreviousPane () {
const panes = this.getPanes()
if (panes.length > 1) {
const currentIndex = panes.indexOf(this.activePane)
let previousIndex = currentIndex - 1
if (previousIndex < 0) { previousIndex = panes.length - 1 }
panes[previousIndex].activate()
return true
} else {
return false
}
}
moveActiveItemToPane (destPane) {
const item = this.activePane.getActiveItem()
if (!destPane.isItemAllowed(item)) { return }
this.activePane.moveItemToPane(item, destPane)
destPane.setActiveItem(item)
}
copyActiveItemToPane (destPane) {
const item = this.activePane.copyActiveItem()
if (item && destPane.isItemAllowed(item)) {
destPane.activateItem(item)
}
}
destroyEmptyPanes () {
for (let pane of this.getPanes()) { if (pane.items.length === 0) { pane.destroy() } }
}
didAddPane (event) {
this.emitter.emit('did-add-pane', event)
const items = event.pane.getItems()
for (let i = 0, length = items.length; i < length; i++) {
const item = items[i]
this.didAddPaneItem(item, event.pane, i)
}
}
willDestroyPane (event) {
this.emitter.emit('will-destroy-pane', event)
}
didDestroyPane (event) {
this.emitter.emit('did-destroy-pane', event)
}
didActivatePane (activePane) {
if (activePane !== this.activePane) {
if (!this.getPanes().includes(activePane)) {
throw new Error('Setting active pane that is not present in pane container')
}
this.activePane = activePane
this.emitter.emit('did-change-active-pane', this.activePane)
this.didChangeActiveItemOnPane(this.activePane, this.activePane.getActiveItem())
}
this.emitter.emit('did-activate-pane', this.activePane)
return this.activePane
}
didAddPaneItem (item, pane, index) {
this.itemRegistry.addItem(item)
this.emitter.emit('did-add-pane-item', {item, pane, index})
}
willDestroyPaneItem (event) {
return this.emitter.emitAsync('will-destroy-pane-item', event)
}
didDestroyPaneItem (event) {
this.itemRegistry.removeItem(event.item)
this.emitter.emit('did-destroy-pane-item', event)
}
didChangeActiveItemOnPane (pane, activeItem) {
if (pane === this.getActivePane()) {
this.emitter.emit('did-change-active-pane-item', activeItem)
this.cancelStoppedChangingActivePaneItemTimeout()
// `setTimeout()` isn't available during the snapshotting phase, but that's okay.
if (typeof setTimeout === 'function') {
this.stoppedChangingActivePaneItemTimeout = setTimeout(() => {
this.stoppedChangingActivePaneItemTimeout = null
this.emitter.emit('did-stop-changing-active-pane-item', activeItem)
}, STOPPED_CHANGING_ACTIVE_PANE_ITEM_DELAY)
}
}
}
cancelStoppedChangingActivePaneItemTimeout () {
if (this.stoppedChangingActivePaneItemTimeout != null) {
clearTimeout(this.stoppedChangingActivePaneItemTimeout)
}
}
}