/*
* 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
*/
/* jshint -W078 */
/**
* This module contains functions for creating Widget option bindings.
*
* @module utils/binding
*/
import { warn } from './log.js';
import { Timer } from './timers.js';
function callAll(a) {
for (let i = 0; i < a.length; i++) {
try {
a[i]();
} catch (err) {
warn('Unsubscribe generated an exception: %o', err);
}
}
}
/**
* This function subscribes to those changes to option `name`
* which are initiated by user action. Returns a function
* which can be called to unsubsribe.
*
* @example
* const unsubcribe = observeUseraction(fader, 'value', (value) => {
* console.log('Fader value changed to %o', value);
* });
*
* setTimeout(unsubscribe, 1000);
*/
export function observeUseraction(widget, name, callback) {
if (!widget.getOptionType(name) && !name.startsWith('_'))
throw new Error('No such options: ' + name);
return widget.subscribe('useraction', (key, value) => {
if (key === name) callback(value);
});
}
/**
* This function subscribes to changes to option `name`.
* Returns a function which can be called to unsubsribe.
*
* @example
* const unsubcribe = observeUseraction(fader, 'value', (value) => {
* console.log('Fader value is %o now.', value);
* });
*
* setTimeout(unsubscribe, 1000);
*/
export function observeOption(widget, name, callback) {
if (!widget.getOptionType(name) && !name.startsWith('_'))
throw new Error('No such options: ' + name);
callback(widget.get(name));
return widget.subscribe('set_' + name, (value) => {
callback(value);
});
}
export function interceptOption(widget, name, callback) {
if (!widget.getOptionType(name) && !name.startsWith('_'))
throw new Error('No such options: ' + name);
return widget.subscribe('userset', (_name, value) => {
if (_name !== name) return;
callback(value);
return false;
});
}
/**
* Class used to create two-way bindings which debounces the transmission of
* backend values to a widget option.
* If the widget supports the generic 'interacting' option, it can be used
* to block incoming values while the user is interacting with the widget.
* Alternatively, incoming values are delayed until a certain time after
* the last value modification was generated by a user.
*/
export class DebounceBinding {
/**
* @param {Object} widget - The Widget.
* @param {String} name - The option name.
* @param {Number} time - Number of milliseconds to delay values passed to
* {@link set} after the last 'useraction' event for the option name.
* @param {Boolean} [use_interacting=true] - If true, use the interacting
* option of the widget to delay incoming values while a user is
* interacting with the widget.
*/
constructor(widget, name, time, use_interacting) {
this.widget = widget;
this.name = name;
this.time = time;
this._timer = new Timer(() => {
this.unlock();
});
this._subscriptions = [() => this._timer.stop()];
this._last_value = null;
this._has_value = false;
this._locked = 0;
if (widget.getOptionType('interacting')) {
let interacting = false;
this._subscriptions.push(
observeOption(widget, 'interacting', (value) => {
if (value === interacting) return;
interacting = value;
if (value) {
this.lock();
} else {
this.unlock();
}
})
);
} else if (!time) {
warn(
"%o does not have an 'interacting' option. Setting debounce time to 0 makes no sense here.",
widget
);
}
if (time) {
this._subscriptions.push(
observeUseraction(widget, name, () => {
if (!this._timer.active) this.lock();
this._timer.restart(time);
})
);
}
}
lock() {
this._locked++;
}
/**
* Returns true if values are currently being debounced.
*/
isLocked() {
return this._locked > 0;
}
unlock() {
if (--this._locked) return;
if (!this._has_value) return;
this.widget.set(this.name, this._last_value);
}
/**
* Receive a value from the backend. Unless the value is delayed by timer or
* by the user interaction, this method calls
* `this.widget.set(this.name, value)'.
*/
set(value) {
this._last_value = value;
this._has_value = true;
if (this._locked) return;
this.widget.set(this.name, value);
}
/**
* Remove all event subscriptions. If the last value received by the backend
* has been delayed, it will not be passed to the widget.
*/
destroy() {
callAll(this._subscriptions);
this._subscriptions = [];
}
}