src/pane-container.js

Summary

Maintainability
D
1 day
Test Coverage
const { find } = require('underscore-plus');
const { Emitter, CompositeDisposable } = require('event-kit');
const Pane = require('./pane');
const ItemRegistry = require('./item-registry');
const { createPaneContainerElement } = 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 = createPaneContainerElement().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 (!global.isGeneratingSnapshot) {
        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);
    }
  }
};