-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implemented infrastructure for settings loading + GUI,
Implemented settings for Gravity example
- Loading branch information
Showing
28 changed files
with
2,721 additions
and
192 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
/** | ||
* @enum(string) | ||
*/ | ||
export const ComponentType = { | ||
solver: "solver", | ||
renderer: "renderer" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) { | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
` |
Oops, something went wrong.