Skip to content

Commit

Permalink
Implemented infrastructure for settings loading + GUI,
Browse files Browse the repository at this point in the history
Implemented settings for Gravity example
  • Loading branch information
DrA1ex committed Sep 29, 2023
1 parent 62b8f4d commit ceaf0a6
Show file tree
Hide file tree
Showing 28 changed files with 2,721 additions and 192 deletions.
583 changes: 583 additions & 0 deletions examples/common/common.css

Large diffs are not rendered by default.

602 changes: 602 additions & 0 deletions examples/common/settings/base.js

Large diffs are not rendered by default.

7 changes: 7 additions & 0 deletions examples/common/settings/enum.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/**
* @enum(string)
*/
export const ComponentType = {
solver: "solver",
renderer: "renderer"
}
112 changes: 112 additions & 0 deletions examples/common/ui/controllers/base.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import {Frame} from "../controls/frame.js";

export class IEventEmitter {
/**
* @abstract
*
* @param {string} type
* @param {*|null} data
*/
emitEvent(type, data = null) {
}
}

export class ControllerBase extends IEventEmitter {
subscribers = new Map();

/**
* @param {HTMLElement} root
* @param {ControllerBase|null} [parentCtrl=null]
*/
constructor(root, parentCtrl = null) {
super();

this.root = root;
this.parentCtrl = parentCtrl;
this.frame = new Frame(this.root);
}

/**
* @param {string} type
* @param {*|null} data
*/
emitEvent(type, data = null) {
for (let subscriptions of this.subscribers.values()) {
const handler = subscriptions[type];
if (handler) {
handler(this, data);
}
}
}

/**
* @param {object} subscriber
* @param {string} type
* @param {function} handler
*/
subscribe(subscriber, type, handler) {
if (!this.subscribers.has(subscriber)) {
this.subscribers.set(subscriber, {});
}

const subscription = this.subscribers.get(subscriber);
subscription[type] = handler;
}

/**
* @param {object} subscriber
* @param {string} type
*/
unsubscribe(subscriber, type) {
const subscription = this.subscribers.has(subscriber) ? this.subscribers.get(subscriber) : null;
if (subscription && subscription.hasOwnProperty(type)) {
delete subscription[type];
}
}
}

/**
* @template T
*/
export class StateControllerBase extends ControllerBase {
static UnsetState = /** @type {T} */ -1;
static STATE_EVENT_NAME = "state";

/**
* @type {T}
*/
currentState = StateControllerBase.UnsetState;

/**
* @param {HTMLElement} root
* @param {ControllerBase|null} [parentCtrl=null]
*/
constructor(root, parentCtrl = null) {
super(root, parentCtrl);
this.parentCtrl?.subscribe(this, StateControllerBase.STATE_EVENT_NAME, (sender, state) => this.setState(state));
}

/**
* @param {T} state
*/
setState(state) {
if (this.currentState === state) {
return;
}

const oldState = this.currentState;
this.currentState = state;

this.emitEvent(StateControllerBase.STATE_EVENT_NAME, this.currentState);
this.onStateChanged(this, oldState, this.currentState);
}

/**
*
* @param {object} sender
* @param {T} oldState
* @param {T} newState
*/
onStateChanged(sender, oldState, newState) {
}
}
246 changes: 246 additions & 0 deletions examples/common/ui/controllers/settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,246 @@
import {ControllerBase} from "./base.js";
import {View} from "../controls/base.js";
import {PropertyType, ReadOnlyProperty} from "../../settings/base.js";
import {Select} from "../controls/select.js";
import {Input, InputType} from "../controls/input.js";
import {Checkbox} from "../controls/checkbox.js";
import {Label} from "../controls/label.js";

import view from "./views/settings.js";

/**
* @template {AppSettingsBase} SettingsT
*/
export class SettingsController extends ControllerBase {
static RECONFIGURE_EVENT = "start_recording";

/** @type {SettingsT} */
settings;
/** @type {{[string]: {[string]: InputControl}}} */
config;
/** @type {Map<Property, {key: string, groupKey: string, group: SettingsGroup, control: InputControl|Label}>} */
propData;

constructor(root, parentCtrl) {
const viewControl = new View(root, view)
super(viewControl.element, parentCtrl);

this.content = this.root.getElementsByClassName("settings-content")[0];
}

/**
* @param {SettingsT} settings
*/
configure(settings) {
this.settings = settings;
this.config = {};
this.propData = new Map();

while (this.content.firstChild) {
this.content.removeChild(this.content.lastChild);
}

for (const [key, group] of Object.entries(this.settings.constructor.Types)) {
if (group.name) {
this.config[key] = {};
this._createBlock(this.config[key], key, group, this.settings[key]);
}
}

for (const prop of this.propData.keys()) {
const {key, groupKey} = this.propData.get(prop);
const deps = this.settings[groupKey].constructor.PropertiesDependencies.get(prop);
if (deps && deps.length > 0) {
for (const depProp of deps) {
if (!(depProp instanceof ReadOnlyProperty)) {
const value = this.settings[groupKey][key];
this.propData.get(depProp).control.setEnabled(!!value);
}
}
}
}
}

onParameterChanged(prop, suppressEvent = false) {
const config = this.getConfig();
const {control, key, groupKey} = this.propData.get(prop);
const value = config[groupKey][key];

if (prop instanceof ReadOnlyProperty) {
control.setText(prop.format(value));
} else {
control.setValue(value);
if (!suppressEvent) {
this.emitEvent(SettingsController.RECONFIGURE_EVENT, config);
}

const deps = config[groupKey].constructor.PropertiesDependencies.get(prop);
if (deps && deps.properties.length > 0) {
for (const depProp of deps.properties) {
this.onParameterChanged(depProp, true);

if (!(depProp instanceof ReadOnlyProperty)) {
const invert = deps.options.invert && (deps.options.invert === true || deps.options.invert[depProp.key] === true);
this.propData.get(depProp).control.setEnabled(invert ? !value : !!value);
}
}
}
}
}

getConfig() {
const config = {};

for (const [blockKey, block] of Object.entries(this.config)) {
const blockConfig = {}
for (const [key, control] of Object.entries(block)) {
let value = control.getValue();
if (value instanceof String) {
value = value && value !== "null" && value.trim() !== "" ? value.trim() : null;
}

blockConfig[key] = value;
}

config[blockKey] = blockConfig;
}

return this.settings.constructor.deserialize(config);
}

_createBlock(config, groupKey, group, value) {
const h3 = document.createElement("h3");
h3.innerText = group.name;
this.content.appendChild(h3);

const block = document.createElement("div");
block.classList.add("settings-block");
this._createBlockEntries(config, groupKey, group, block, value);
this.content.appendChild(block);
}

/**
* @param {object} config
* @param {string} groupKey
* @param {SettingsGroup} group
* @param {HTMLElement} parent
* @param {SettingsBase} groupValue
* @private
*/
_createBlockEntries(config, groupKey, group, parent, groupValue) {
let count = 0;
for (const [key, prop] of Object.entries(groupValue.constructor.Properties)) {
const caption = document.createElement("div");
caption.innerText = prop.name || key;
caption.classList.add("settings-caption")
if (prop.description) {
caption.setAttribute("data-tooltip", prop.description);
}
parent.appendChild(caption);


const control = this._createBlockInput(prop, groupValue[key]);
control.addClass("settings-input");
control.setOnChange(() => this.onParameterChanged(prop));

this.propData.set(prop, {
key,
groupKey,
group,
control
});

parent.appendChild(control.element);

config[key] = control;
count += 1;
}

for (const [key, prop] of Object.entries(groupValue.constructor.ReadOnlyProperties)) {
const caption = document.createElement("div");
caption.innerText = prop.name || key;
caption.classList.add("settings-caption")
if (prop.description) {
caption.setAttribute("data-tooltip", prop.description);
}
parent.appendChild(caption);

const label = new Label(document.createElement("div"));
label.setText(prop.format(groupValue[key]));
label.addClass("settings-input");
parent.appendChild(label.element);

this.propData.set(prop, {
key,
groupKey,
group,
control: label
});

count += 1;
}

parent.style.gridTemplateRows = `repeat(${count}, 2em)`;
}

/**
* @param {Property} property
* @param {*} value
* @returns {InputControl}
* @private
*/
_createBlockInput(property, value) {
let control;
switch (property.type) {
case PropertyType.enum:
control = this._createSelect(property.enumType, value);
break;

case PropertyType.int:
control = this._createInput(value, InputType.int)
break;

case PropertyType.float:
control = this._createInput(value, InputType.float)
break;

case PropertyType.bool:
control = this._createCheckbox(value);
break;

default:
case PropertyType.string:
control = this._createInput(value, InputType.text)
break;
}

return control;
}

_createInput(value, type) {
const input = new Input(document.createElement("input"), type);
input.setValue(value);

return input;
}

_createCheckbox(value) {
const e = document.createElement("input");
const input = new Checkbox(e);
input.setValue(value);

return input;
}

_createSelect(type, value) {
const select = new Select(document.createElement("select"));
select.setOptions(Object.keys(type));

const entry = value && Object.entries(type).find(([k, v]) => v === value);
if (entry) {
select.select(entry[0]);
}

return select;
}
}
13 changes: 13 additions & 0 deletions examples/common/ui/controllers/views/settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// language=HTML
export default `
<div>
<div class="dialog-caption settings-dialog-caption">
Settings
<button class="dialog-close settings-close icon-btn">
</button>
</div>
<div class="settings-base settings-content flex-column">
</div>
</div>
`
Loading

0 comments on commit ceaf0a6

Please sign in to comment.