From af2950c3d21b6f12ddab60d4bde74875a0e9519a Mon Sep 17 00:00:00 2001
From: Ad5001 <mail@ad5001.eu>
Date: Thu, 10 Oct 2024 06:49:14 +0200
Subject: [PATCH] Starting Settings modules + implemented basic events for
 ECMAScript <=> Qt compat.

---
 common/src/events.mjs           |  96 ++++++++++++++++++
 common/src/lib/polyfills/js.mjs |   4 +-
 common/src/module/common.mjs    |   4 +-
 common/src/module/interface.mjs |   4 +-
 common/src/module/settings.mjs  | 173 ++++++++++++++++++++++++++++++++
 5 files changed, 276 insertions(+), 5 deletions(-)
 create mode 100644 common/src/events.mjs
 create mode 100644 common/src/module/settings.mjs

diff --git a/common/src/events.mjs b/common/src/events.mjs
new file mode 100644
index 0000000..f1ec970
--- /dev/null
+++ b/common/src/events.mjs
@@ -0,0 +1,96 @@
+/**
+ *  LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
+ *  Copyright (C) 2021-2024  Ad5001
+ *
+ *  This program 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.
+ *
+ *  This program 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, see <https://www.gnu.org/licenses/>.
+ */
+
+/**
+ * We do not inherit the DOM's Event, because not only the DOM part is unnecessary,
+ * but also because it does not exist within Qt environments.
+ */
+
+
+export class BaseEvent {
+    /**
+     * @property {string} name - Name of the event.
+     */
+    constructor(name) {
+        this.name = name
+    }
+}
+
+
+/**
+ * Base class for all classes which can emit events.
+ */
+export class BaseEventEmitter {
+    static emits = []
+    
+    /** @type {Record<string, function[]>} */
+    #listeners = {}
+    
+    constructor() {
+        for(const eventType of this.constructor.emits) {
+            this.#listeners[eventType] = new Set()
+        }
+    }
+    
+    /**
+     * Adds a listener to an event that can be emitted by this object.
+     * 
+     * @param {string} eventType - Name of the event to listen to. Throws an error if this object does not emit this kind of event.
+     * @param {function(BaseEvent)} eventListener - The function to be called back when the event is emitted.
+     */
+    addEventListener(eventType, eventListener) {
+        if(!this.emits.includes(eventType)) {
+            const className = this.constructor.name
+            const eventTypes = this.constructor.emits.join(", ")
+            throw new Error(`Cannot listen to unknown event ${eventType} in class ${className}. ${className} only emits: ${eventTypes}`)
+        }
+        if(!this.#listeners[eventType].has(eventListener))
+            this.#listeners[eventType].add(eventListener)
+    }
+    
+    /**
+     * Remvoes a listener from an event that can be emitted by this object.
+     * 
+     * @param {string} eventType - Name of the event that was listened to. Throws an error if this object does not emit this kind of event.
+     * @param {function(BaseEvent)} eventListener - The function previously registered as a listener.
+     * @returns {boolean} True if the listener was removed, false if it was not found.
+     */
+    removeEventListener(eventType, eventListener) {
+        if(!this.emits.includes(eventType)) {
+            const className = this.constructor.name
+            const eventTypes = this.constructor.emits.join(", ")
+            throw new Error(`Cannot listen to unknown event ${eventType} in class ${className}. ${className} only emits: ${eventTypes}`)
+        }
+        return this.#listeners[eventType].delete(eventListener)
+        
+    }
+    
+    /**
+     * Emits an event to all of its listeners.
+     * 
+     * @param {BaseEvent} e
+     */
+    emit(e) {
+        if(!(e instanceof BaseEvent))
+            throw new Error("Cannot emit non event object.")
+        if(!this.emits.includes(e.name))
+            throw new Error(`Cannot emit event '${e.name}' from class ${this.constructor.name}. ${this.constructor.name} can only emits: ${this.constructor.emits.join(", ")}.`)
+        for(const listener of this.#listeners[e.name])
+            listener(e)
+    }
+}
diff --git a/common/src/lib/polyfills/js.mjs b/common/src/lib/polyfills/js.mjs
index 43ed375..9bcf80c 100644
--- a/common/src/lib/polyfills/js.mjs
+++ b/common/src/lib/polyfills/js.mjs
@@ -1,4 +1,4 @@
-/**
+/*!
  *  LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
  *  Copyright (C) 2021-2024  Ad5001
  *
@@ -123,4 +123,4 @@ for(const [year, entries] of Object.entries(polyfills)) {
     for(const [context, functionName, polyfill] of entries.filter(x => x[0][x[1]] === undefined)) {
         context[functionName] = polyfill
     }
-}
\ No newline at end of file
+}
diff --git a/common/src/module/common.mjs b/common/src/module/common.mjs
index 5fc9387..bdca62f 100644
--- a/common/src/module/common.mjs
+++ b/common/src/module/common.mjs
@@ -17,6 +17,7 @@
  */
 
 import { Interface } from "./interface.mjs"
+import { BaseEventEmitter } from "../events.mjs"
 
 // Define Modules interface before they are imported.
 globalThis.Modules = globalThis.Modules || {}
@@ -24,7 +25,7 @@ globalThis.Modules = globalThis.Modules || {}
 /**
  * Base class for global APIs in runtime.
  */
-export class Module {
+export class Module extends BaseEventEmitter {
     /** @type {string} */
     #name
     /** @type {Object.<string, (Interface|string|number|boolean)>} */
@@ -36,6 +37,7 @@ export class Module {
      * @param {Object.<string, (Interface|string|number|boolean)>} initializationParameters - List of parameters for the initialize function.
      */
     constructor(name, initializationParameters = {}) {
+        super()
         console.log(`Loading module ${name}...`)
         this.#name = name
         this.#initializationParameters = initializationParameters
diff --git a/common/src/module/interface.mjs b/common/src/module/interface.mjs
index 9273c8f..f6e5a13 100644
--- a/common/src/module/interface.mjs
+++ b/common/src/module/interface.mjs
@@ -1,4 +1,4 @@
-/*!
+/**
  * LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
  * 
  * @author Ad5001 <mail@ad5001.eu>
@@ -184,4 +184,4 @@ export class HelperInterface extends Interface {
      * @returns {string} the loaded data - just JSON encoded, requires the "LPFv1" mime to be stripped
      */
     load = FUNCTION
-}
\ No newline at end of file
+}
diff --git a/common/src/module/settings.mjs b/common/src/module/settings.mjs
new file mode 100644
index 0000000..4487dde
--- /dev/null
+++ b/common/src/module/settings.mjs
@@ -0,0 +1,173 @@
+/**
+ *  LogarithmPlotter - 2D plotter software to make BODE plots, sequences and distribution functions.
+ *  Copyright (C) 2021-2024  Ad5001
+ *
+ *  This program 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.
+ *
+ *  This program 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, see <https://www.gnu.org/licenses/>.
+ */
+
+import { Module } from "./common.mjs"
+import { BaseEvent } from "../events.mjs"
+
+
+/**
+ * Base event for when a setting was changed.
+ */
+class ChangedEvent extends BaseEvent {
+    /**
+     * 
+     * @param {string} property - Name of the property that was chagned
+     * @param {string|number|boolean} oldValue - Old value of the property
+     * @param {string|number|boolean} newValue - Current (new) value of the property
+     * @param {boolean} byUser - True if the user is at the source of the change in the setting.
+     */
+    constructor(property, oldValue, newValue, byUser) {
+        super("changed")
+        
+        this.property = property
+        this.oldValue = oldValue
+        this.newValue = newValue
+        this.byUser = byUser
+    }
+}
+
+/**
+ * Module for graph settings.
+ */
+class SettingsAPI extends Module {
+    static emits = ["changed"]
+    
+    #properties = new Map([
+        ['xzoom', 100],
+        ['yzoom', 10],
+        ['xmin', .5],
+        ['ymax', 25],
+        ['xaxisstep', "4"],
+        ['yaxisstep', "4"],
+        ['xlabel', ""],
+        ['ylabel', ""],
+        ['linewidth', 1],
+        ['textsize', 18],
+        ['logscalex', true],
+        ['showxgrad', true],
+        ['showygrad', true],
+    ])
+    
+    constructor() {
+        super("Settings", {
+            helper: HelperInterface
+        })
+    }
+    
+    initialize({ helper }) {
+        super.initialize({ helper })
+        // Initialize default values.
+        for(const key of this.#properties.keys()) {
+            switch(typeof this.#properties.get(key)) {
+                case 'boolean':
+                    this.set(key, helper.getSettingBool(key), false)
+                    break
+                case 'number':
+                    this.set(key, helper.getSettingInt(key), false)
+                    break
+                case 'string':
+                    this.set(key, helper.getSetting(key), false)
+                    break
+            }
+        }
+    }
+    
+    /**
+     * Sets a setting to a given value
+     * 
+     * @param {boolean} byUser - Set to true if the user is at the origin of this change. 
+     */
+    set(property, value, byUser) {
+        if(!this.#properties.has(property))
+            throw new Error(`Property ${property} is not a setting.`)
+        const oldValue = this.#properties.get(property)
+        const propType = typeof oldValue
+        if(propType !== typeof value)
+            throw new Error(`Value of ${property} must be a ${propType}.`)
+        this.#properties.set(property, value)
+        this.emit(new ChangedEvent(property, oldValue, value, byUser === true))
+    }
+    
+    /**
+     * Zoom on the x axis of the diagram.
+     * @returns {number}
+     */
+    get xzoom() { return this.#properties.get("xzoom"); }
+    /**
+     * Zoom on the y axis of the diagram.
+     * @returns {number}
+     */
+    get yzoom() { return this.#properties.get("yzoom"); }
+    /**
+     * Minimum x of the diagram.
+     * @returns {number}
+     */
+    get xmin() { return this.#properties.get("xmin"); }
+    /**
+     * Maximum y of the diagram.
+     * @returns {number}
+     */
+    get ymax() { return this.#properties.get("ymax"); }
+    /**
+     * Step of the x axis graduation (expression).
+     * @note Only available in non-logarithmic mode.
+     * @returns {string}
+     */
+    get xaxisstep() { return this.#properties.get("xaxisstep"); }
+    /**
+     * Step of the y axis graduation (expression).
+     * @returns {string}
+     */
+    get yaxisstep() { return this.#properties.get("yaxisstep"); }
+    /**
+     * Label used on the x axis.
+     * @returns {string}
+     */
+    get xlabel() { return this.#properties.get("xlabel"); }
+    /**
+     * Label used on the y axis.
+     * @returns {string}
+     */
+    get ylabel() { return this.#properties.get("ylabel"); }
+    /**
+     * Width of lines that will be drawn into the canvas.
+     * @returns {number}
+     */
+    get linewidth() { return this.#properties.get("linewidth"); }
+    /**
+     * Font size of the text that will be drawn into the canvas.
+     * @returns {number}
+     */
+    get textsize() { return this.#properties.get("textsize"); }
+    /**
+     * true if the canvas should be in logarithmic mode, false otherwise.
+     * @returns {boolean}
+     */
+    get logscalex() { return this.#properties.get("logscalex"); }
+    /**
+     * true if the x graduation should be shown, false otherwise.
+     * @returns {boolean}
+     */
+    get showxgrad() { return this.#properties.get("showxgrad"); }
+    /**
+     * true if the y graduation should be shown, false otherwise.
+     * @returns {boolean}
+     */
+    get showygrad() { return this.#properties.get("showygrad"); }
+    
+}