src/widgets/pluginControls.js

const Jalv = require("../jalv");
const blessed = require("blessed");
const contrib = require("blessed-contrib");
const PubSub = require("pubsub-js");
const Layout = require("../layout");
const store = require("../store");
const { settings } = require("../../settings");

const PluginControls = function (grid, x, y, xSpan, ySpan) {
  const pluginControls = grid.set(y, x, ySpan, xSpan, blessed.box, {
    label: "Plugin Parameters",
    input: true,
    mouse: true,
    interactive: true,
    keys: true,
    padding: { left: 1, right: 1 },
    mouse: true,
    scrollable: true,

    style: {
      scrollbar: true,
      focus: {
        border: { fg: "red" },
        //   enabled: false,
      },
    },
  });

  pluginControls.key(["down"], (e, x) => {
    Layout.focusNext();
  });

  const plugin = store.getSelectedPlugin();

  PubSub.subscribe("pluginControlsChanged", update);
  PubSub.subscribe("selectedPlugin", (msg, plugin) => {
    if (!plugin) {
      update("", null);
    }
  });
  //  Update all controls for a given plugin (queryin jalv)
  async function update(msg, plugin) {
    const length = JSON.parse(JSON.stringify(pluginControls.children.length));
    for (let index = length - 1; index > 0; index--) {
      const element = pluginControls.children[index];
      element.hide();
    }

    if (!plugin) return;

    // Store the control widget on the plugin instance:
    if (!plugin.info.controlWidgets) plugin.info.controlWidgets = {};

    // const values = await Jalv.getControls(plugin, "controls");
    const values = plugin.info.controls;

    let y = 0;
    plugin.ports.control.input.forEach((control) => {
      if (!plugin.info.controlWidgets[control.symbol]) {
        controlWidget = progressControl(
          values[control.symbol],
          y,
          control,
          plugin
        );
        pluginControls.append(controlWidget);
        plugin.info.controlWidgets[control.symbol] = controlWidget;
      } else {
        plugin.info.controlWidgets[control.symbol].setValue(
          values[control.symbol]
        );
        plugin.info.controlWidgets[control.symbol].show();
      }
      y += 2;
    });
  }

  return pluginControls;
};

/**
 *  Initializes a plugin control widget.
 *   // {
  // 	"comment": null,
  // 	"designation": null,
  // 	"index": 6,
  // 	"name": "Master level",
  // 	"properties": [],
  // 	"rangeSteps": null,
  // 	"ranges": { "default": 0.0, "maximum": 30.0, "minimum": -30.0 },
  // 	"scalePoints": [],
  // 	"shortName": "Master level",
  // 	"symbol": "master",
  // 	"units": { "label": "decibels", "render": "%f dB", "symbol": "dB" }
  //   },
 *
 * @param {number} value focused
 * @param {number} top
 * @param {pluginControl} pluginControl 
 * @param {pluginInstance} pluginInstance
 * @returns Returns the progress control blessed widget.
 */
function progressControl(value, top, pluginControl, pluginInstance) {
  const {
    comment,
    designation,
    index,
    name,
    properties,
    rangeSteps,
    ranges,
    scalePoints,
    shortName,
    symbol,
    units,
  } = pluginControl;

  var box = blessed.box({
    interactive: false,
    focusable: false,
    top: top,
  });

  box.value = parseControlValue(pluginControl, value);
  const valuePercent = getControlValuePercent(pluginControl, box.value);

  //    Control Label
  var label = blessed.text({
    content: shortName,
    left: 1,
    top: 1,
    interactive: false,
    focusable: false,
  });

  //    Progress Widget
  var progress = blessed.progressbar({
    border: {
      type: "bg",
      //   fg: "#882822",
      //   bg: "#512725",
    },
    style: {
      bg: "#282828",
      focus: {
        bg: "#1e1e1e",
        border: {
          fg: "#637373",
        },
        bar: {
          bg: "#68955d",
        },
      },
    },
    input: true,
    ch: "░",
    height: 3,
    top: 0,
    left: "35%",
    right: "8",
    filled: parseInt(valuePercent.toString()),
    width: "65%",
  });

  const valueLabelValue = getControlValueLabel(
    pluginControl,
    box.value.toFixed(2)
  );

  var valueLabel = blessed.text({
    content: valueLabelValue,
    right: 4,
    top: 1,
    interactive: false,
    focusable: false,
  });

  box.append(label);
  box.append(progress);
  box.append(valueLabel);

  box.updateValue = function (val) {
    Jalv.setControl(pluginInstance, pluginControl, val);
    const newValue = parseControlValue(pluginControl, val);

    box.value = newValue;
    const _valuePercent = getControlValuePercent(pluginControl, newValue);
    const _valueLabel = getControlValueLabel(
      pluginControl,
      newValue.toFixed(2)
    );
    progress.setProgress(_valuePercent);
    valueLabel.setContent(_valueLabel);
  };

  box.setValue = function (val) {
    const newValue = parseControlValue(pluginControl, val);

    box.value = newValue;
    const _valuePercent = getControlValuePercent(pluginControl, newValue);
    const _valueLabel = getControlValueLabel(
      pluginControl,
      newValue.toFixed(2)
    );
    progress.setProgress(_valuePercent);
    valueLabel.setContent(_valueLabel);
  };

  //   Keyboard action
  progress.key(
    [
      "right",
      "C-right",
      "S-right",
      "left",
      "C-left",
      "S-left",
      "pageup",
      "pagedown",
    ],
    function (e, keys) {
      let newValue = 0;

      // if it is a toggle button, just send 0 or 1
      if (properties.includes("toggled")) {
        if (keys.name === "right") {
          newValue = 1;
        }
      } else {
        let step = settings.DEFAULT_CONTROL_STEP;

        // For small values, (less than 1, make the steps even smaller)
        if (ranges.maximum - ranges.minimum < 1) {
          step = (ranges.maximum - ranges.minimum) / 10;
        }

        // For big values make bigger steps.
        // Need to test if could affect a 'sensible' knob that for example would blast the volume up.
        if (ranges.maximum - ranges.minimum > 100) {
          step = (ranges.maximum - ranges.minimum) / 10;
        }

        // TODO: Not working properly
        if (properties.includes("logarithmic")) {
          // This value indicates into how many evenly-divided points the (control) port range should be divided for step-wise control. This may be used for changing the value with step-based controllers like arrow keys, mouse wheel, rotary encoders, and so on.
          // Note that when used with a logarithmic port, the steps are logarithmic too, and port value can be calculated as:
          // value = lower * pow(upper / lower, step / (steps - 1))
          // and the step from value is:
          //   // step = (steps - 1) * log(value / lower) / log(upper / lower)
          //   step =
          //     (9 * Math.log(box.value / ranges.minimum)) /
          //     Math.log(ranges.maximum / ranges.minimum);
          //   store.wlogDebug("Log Scale");
        }

        if (keys.shift) step /= 10;
        if (keys.ctrl) step *= 5;
        if (keys.name === "pageup") step = -ranges.maximum / 5;
        if (keys.name === "pagedown") step = ranges.maximum / 5;
        if (keys.name === "left") step = -step;
        newValue = box.value + step;

        if (ranges) {
          if (newValue < ranges.minimum) newValue = ranges.minimum;
          if (newValue > ranges.maximum) newValue = ranges.maximum;
        }
      }
      box.updateValue(newValue);
    }
  );

  //   progress.key(["home", "end"], function (e, key) {
  //     if (key.name === "home") Layout.focusPrev();
  //     else if (key.name === "end") Layout.focusNext();
  //   });

  progress.key(["up", "down"], function (e, key) {
    if (key.name === "up") Layout.focusPrev();
    else if (key.name === "down") Layout.focusNext();
  });

  //   progress.key("S-right", function (a, b) {
  //     box.updateValue(box.value + settings.DEFAULT_CONTROL_MEDIUM_STEP);
  //   });

  //   progress.key("left", function (a, b) {
  //     box.updateValue(box.value - settings.DEFAULT_CONTROL_SMALL_STEP);
  //   });

  //   progress.key("S-right", function (a, b) {
  //     box.updateValue(box.value + settings.DEFAULT_CONTROL_MEDIUM_STEP);
  //   });
  return box;
}

/**
 * Returns a relative percent of a value for a plugin control.
 * Uses rangeMax and RangeMin
 *
 * @param {puglinControl} control plugin control.
 * @param {number} value Value to calculate %
 */
function getControlValuePercent(control, value) {
  const parsedValue = control.properties.includes("integer")
    ? parseInt(value)
    : parseFloat(value);
  const valuePercent =
    // (parsedValue / (control.ranges.minimum + control.ranges.maximum)) * 100;
    ((parsedValue - control.ranges.minimum) /
      (control.ranges.maximum - control.ranges.minimum)) *
    100;

  return valuePercent;
}

/**
 * Creates a label for a specific control.
 *
 * @param {*} control Plugin control
 * @param {*} value Value to append to the label
 * @returns Returns a label with units if applicable and formatted according to the LV2 specification.
 */
function getControlValueLabel(control, value) {
  let valueLabelValue = value;
  const parsedValue = parseControlValue(control, value);
  if (Object.keys(control.units).length > 0) {
    valueLabelValue = control.units.render.replace("%f", valueLabelValue);
  }
  ///"units": { "label": "decibels", "render": "%f dB", "symbol": "dB" }
  if (control.properties.includes("toggled")) {
    valueLabelValue = parsedValue === 1 ? "ON" : "OFF";
  }
  if (control.properties.includes("enumeration")) {
    const options = control.scalePoints.length;

    const option = control.scalePoints.filter((x) => x.value === parsedValue);

    if (option[0]) valueLabelValue = option[0].label;
  }

  return valueLabelValue;
}

/**
 * Parse a control value, usually a float except if defined in properties.
 * TODO: Check other cases.
 *
 * @param {pluginControl} control
 * @param {string} value
 * @returns a float or an int with the value
 */
function parseControlValue(control, value) {
  const parsedValue = control.properties.includes("integer")
    ? parseInt(value)
    : parseFloat(value);
  return parsedValue;
}

exports.parseControlValue = parseControlValue;
exports.getControlValueLabel = getControlValueLabel;
exports.getControlValuePercent = getControlValuePercent;
exports.PluginControls = PluginControls;