matrix/widgets/matrix.js

/*
 * This file is part of AUX.
 *
 * AUX is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public
 * License as published by the Free Software Foundation; either
 * version 3 of the License, or (at your option) any later version.
 *
 * AUX is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General
 * Public License along with this program; if not, write to the
 * Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA  02110-1301  USA
 */

import { defineChildWidget } from './../../child_widget.js';
import { innerWidth, outerWidth, addClass } from './../../utils/dom.js';
import { defineRender } from '../../renderer.js';

import { Indicators } from './indicators.js';
import { Indicator } from './indicator.js';
import { Patchbay } from './patchbay.js';
import { VirtualTree } from './virtualtree.js';
import { ScrollDetector } from './scroll_detector.js';

import { ConnectionDataView } from '../models.js';

const scrollDetectorTimeout = 200;

function setVirtualtreeviews() {
  var O = this.options;
  if (!O.sources || !O.sinks) return;
  switch (O.signal_flow) {
    case 'top-left': {
      const connectionview = new ConnectionDataView(O.sinks, O.sources);
      this.connectionview = connectionview;
      this.virtualtree_top.set('virtualtreeview', O.sources);
      this.virtualtree_left.set('virtualtreeview', O.sinks);
      this.indicators.set('connectionview', connectionview);
      break;
    }
    case 'left-top': {
      const connectionview = new ConnectionDataView(O.sources, O.sinks);
      this.connectionview = connectionview;
      this.virtualtree_left.set('virtualtreeview', O.sources);
      this.virtualtree_top.set('virtualtreeview', O.sinks);
      this.indicators.set('connectionview', connectionview);
      break;
    }
  }
}

/**
 * A patchbay widget for handling connections between sources and sinks
 * with matrix layout.
 * It includes two {@link VirtualTree}s on top and left hand side and a
 * {@link Indicators} widget displaying and handling the connections.
 *
 * @param {Object} [options={ }] - An object containing initial options.
 *
 * @property {Object} [options.indicator_class=Indicator] - The class to derive the
 *   indicators inside the {@link Indicators} from.
 * @property {String} [options.signal_flow='left-top'] - Define the direction of
 *   the signal flow. Can be either `top-left` or `left-top`. This defines
 *   the position of sinks and sources {@link VirtualTreeView} on the
 *   screen.
 *
 * @extends Patchbay
 *
 * @class Matrix
 */
export class Matrix extends Patchbay {
  static get _options() {
    return {
      _virtualtree_size: 'number',
      indicator_class: 'object',
      signal_flow: 'string',
    };
  }

  static get options() {
    return {
      _virtualtree_size: 0,
      indicator_class: Indicator,
      signal_flow: 'left-top',
    };
  }

  static get static_events() {
    return {
      set_signal_flow: setVirtualtreeviews,
      set_sources: setVirtualtreeviews,
      set_sinks: setVirtualtreeviews,
    };
  }

  static get renderers() {
    return [
      defineRender('_virtualtree_size', function (_virtualtree_size) {
        const virtualtree = this.virtualtree_top;
        virtualtree.element.style.height = _virtualtree_size + 'px';
        virtualtree.triggerResize();
      }),
    ];
  }

  /**
   * Returns the virtual tree view instance of the left tree.
   */
  getVirtualTreeViewLeft() {
    return this.virtualtree_left.get('virtualtreeview');
  }

  /**
   * Returns the virtual tree view instance of the top tree.
   */
  getVirtualTreeViewTop() {
    return this.virtualtree_top.get('virtualtreeview');
  }

  initialize(options) {
    super.initialize(options);
    this.connectionview = null;
    this._scroll_left = new ScrollDetector(scrollDetectorTimeout);
    this._scroll_top = new ScrollDetector(scrollDetectorTimeout);
    this._scroll_matrix = new ScrollDetector(scrollDetectorTimeout);
  }

  scrollTo(options) {
    if (options.top >= 0) this.virtualtree_left.scrollTo(options.top);
    if (options.left >= 0) this.virtualtree_top.scrollTo(options.left);
  }

  getScrollPosition() {
    const top = this.virtualtree_left.getScrollTop();
    const left = this.virtualtree_top.getScrollTop();

    return { top, left };
  }

  draw(options, element) {
    const O = this.options;
    addClass(this.element, 'aux-matrix');
    super.draw(options, element);

    this.virtualtree_left.on('scrollTopChanged', (position) => {
      let called = false;
      this._scroll_left.maybeScrollEvent(() => {
        this._scroll_matrix.maybeScrollTo(() => {
          called = true;
          this.indicators.scrollTopTo(position);
        });
      });
      if (!called) return false;
    });
    this.virtualtree_top.on('scrollTopChanged', (position) => {
      let called = false;
      this._scroll_top.maybeScrollEvent(() => {
        this._scroll_matrix.maybeScrollTo(() => {
          called = true;
          this.indicators.scrollLeftTo(position);
        });
      });
      if (!called) return false;
    });
    this.indicators.on('scrollChanged', (yposition, xposition) => {
      this._scroll_matrix.maybeScrollEvent(() => {
        this._scroll_left.maybeScrollTo(() => {
          this.virtualtree_left.scrollTo(yposition);
          Promise.resolve().then(() => {
            this.virtualtree_left._scrollDataTo(yposition);
          });
        });
        this._scroll_top.maybeScrollTo(() => {
          this.virtualtree_top.scrollTo(xposition);
          Promise.resolve().then(() => {
            this.virtualtree_top._scrollDataTo(xposition);
          });
        });
      });
    });
    this.indicators.on('indicatorClicked', (source, sink) => {
      this.emit('toggleConnection', source, sink);
    });

    this.virtualtree_top.scroll_y.drag.set('direction', 'horizontal');
    this.virtualtree_top.scroll_y.drag.set('reverse', true);
    setVirtualtreeviews.call(this);
  }

  resize() {
    this.set(
      '_virtualtree_size',
      innerWidth(this.element) - outerWidth(this.virtualtree_left.element)
    );
    super.resize();
  }

  destroy() {
    this._scroll_left.destroy();
    this._scroll_top.destroy();
    this._scroll_matrix.destroy();
    super.destroy();
  }
}
/**
 * @member {VirtualTree} Matrix#virtualtree_left - The {@link VirtualTree}
 *   on the left hand side. Has class <code>.aux-virtualtreeleft</code>.
 */
defineChildWidget(Matrix, 'virtualtree_left', {
  create: VirtualTree,
  show: true,
  map_options: {
    size: 'size',
    entry_class: 'entry_class',
  },
  default_options: {
    class: 'aux-virtualtreeleft',
  },
});
/**
 * @member {VirtualTree} Matrix#virtualtree_left - The {@link VirtualTree}
 *   on top. Has class <code>.aux-virtualtreetop</code>.
 */
defineChildWidget(Matrix, 'virtualtree_top', {
  create: VirtualTree,
  show: true,
  map_options: {
    size: 'size',
    entry_class: 'entry_class',
  },
  default_options: {
    class: 'aux-virtualtreetop',
  },
});
/**
 * @member {Indicators} Matrix#indicators - The {@link Indicators}
 *   widget. Has class <code>.aux-indicators</code>.
 */
defineChildWidget(Matrix, 'indicators', {
  create: Indicators,
  show: true,
  map_options: {
    size: 'size',
    indicator_class: 'indicator_class',
  },
  static_events: {
    connectDiagonal: function (...args) {
      this.parent.emit('connectDiagonal', ...args);
    },
    disconnectDiagonal: function (...args) {
      this.parent.emit('disconnectDiagonal', ...args);
    },
    disconnectAll: function (...args) {
      this.parent.emit('disconnectAll', ...args);
    },
  },
});