From 6a1f01ba1fdb410ec491f68db02a8efd9e548ef9 Mon Sep 17 00:00:00 2001 From: Ad5001 Date: Tue, 24 Sep 2024 00:04:11 +0200 Subject: [PATCH] Better interfaces! --- .../LogarithmPlotter/LogGraphCanvas.qml | 2 +- .../LogarithmPlotter/js/module/canvas.mjs | 46 +------ .../LogarithmPlotter/js/module/common.mjs | 24 +--- .../LogarithmPlotter/js/module/history.mjs | 16 +-- .../LogarithmPlotter/js/module/interface.mjs | 130 ++++++++++++++++++ .../ad5001/LogarithmPlotter/js/module/io.mjs | 51 +------ 6 files changed, 155 insertions(+), 114 deletions(-) create mode 100644 LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/module/interface.mjs diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/LogGraphCanvas.qml b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/LogGraphCanvas.qml index 2d7eb08..e54781f 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/LogGraphCanvas.qml +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/LogGraphCanvas.qml @@ -140,7 +140,7 @@ Canvas { id: drawingErrorDialog title: qsTranslate("expression", "LogarithmPlotter - Drawing error") text: "" - function showDialog(objType, objName, error) { + function show(objType, objName, error) { text = qsTranslate("error", "Error while attempting to draw %1 %2:\n%3\n\nUndoing last change.").arg(objType).arg(objName).arg(error) open() } diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/module/canvas.mjs b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/module/canvas.mjs index 75efb04..04abf68 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/module/canvas.mjs +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/module/canvas.mjs @@ -17,60 +17,28 @@ */ import { Module } from "./common.mjs" +import { FUNCTION, Interface, CanvasInterface, DialogInterface } from "./interface.mjs" import { textsup } from "../utils.mjs" import { Expression } from "../math/index.mjs" import Latex from "./latex.mjs" import Objects from "./objects.mjs" import History from "./history.mjs" - -/** - * @typedef {Settings} Canvas - * @property {object} imageLoaders - * @property {function()} requestPaint - * @property {function(string)} getContext - * @property {function(rect)} markDirty - * @property {function(string)} loadImage - */ - class CanvasAPI extends Module { constructor() { super("Canvas", { - canvas: { - width: "number", - height: "number", - xmin: "number", - ymax: "number", - xzoom: "number", - yzoom: "number", - xaxisstep: "number", - yaxisstep: "number", - xlabel: "string", - ylabel: "string", - linewidth: "number", - textsize: "number", - logscalex: "boolean", - showxgrad: "boolean", - showygrad: "boolean", - imageLoaders: "object", - getContext: Function, - markDirty: Function, - loadImage: Function, - requestPaint: Function, - }, - drawingErrorDialog: { - showDialog: Function, - } + canvas: CanvasInterface, + drawingErrorDialog: DialogInterface }) - /** @type {Canvas} */ + /** @type {CanvasInterface} */ this._canvas = null /** @type {CanvasRenderingContext2D} */ this._ctx = null /** - * @type {{showDialog(string, string, string)}} + * @type {{show(string, string, string)}} * @private */ this._drawingErrorDialog = null @@ -94,8 +62,8 @@ class CanvasAPI extends Module { /** * Initialize the module. - * @param {Canvas} canvas - * @param {{showDialog(string, string, string)}} drawingErrorDialog + * @param {CanvasInterface} canvas + * @param {{show(string, string, string)}} drawingErrorDialog */ initialize({ canvas, drawingErrorDialog }) { super.initialize({ canvas, drawingErrorDialog }) diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/module/common.mjs b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/module/common.mjs index 9cd7bef..6902745 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/module/common.mjs +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/module/common.mjs @@ -16,6 +16,8 @@ * along with this program. If not, see . */ +import { Interface } from "./interface.mjs" + /** * Base class for global APIs in runtime. */ @@ -24,7 +26,7 @@ export class Module { /** * * @param {string} name - Name of the API - * @param {Object.|string[]|string|object)>} initializationParameters - List of parameters for the initialize function. + * @param {Object.} initializationParameters - List of parameters for the initialize function. */ constructor(name, initializationParameters = {}) { console.log(`Loading module ${name}...`) @@ -43,23 +45,9 @@ export class Module { for(const [name, value] of Object.entries(this.__initializationParameters)) { if(!options.hasOwnProperty(name)) throw new Error(`Option '${name}' of initialize of module ${this.__name} does not exist.`) - if(typeof value === "object") { - if(value instanceof Array) - for(const k in value) - if(!options[name].hasOwnProperty(k)) - throw new Error(`Option '${name}' of initialize of module ${this.__name} does not have the property '${k}'.`) - else - for(const [k, v] in Object.entries(value)) { - if(!options[name].hasOwnProperty(k)) - throw new Error(`Option '${name} of initialize of module ${this.__name} does not have the property '${k}'.`) - else if(typeof (v) === "string" && typeof (options[name][k]) !== v) - throw new Error(`Property '${k}' of initialize option ${name} of module ${this.__name}'s type is not '${v}'.`) - else if(typeof (v) === "object" && !(options[name][k] instanceof v)) - throw new Error(`Property '${k}' of initialize option ${name} of module ${this.__name} is not a '${v}'.`) - } - - - } else if(typeof value === "string" && typeof options[name] !== value) + if(typeof value === "function" && value.prototype instanceof Interface) + Interface.check_implementation(value, options[name]) + else if(typeof value !== typeof options[name]) throw new Error(`Option '${name}' of initialize of module ${this.__name} is not a '${value}' (${typeof options[name]}).`) } this.initialized = true diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/module/history.mjs b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/module/history.mjs index f773402..09b45d6 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/module/history.mjs +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/module/history.mjs @@ -18,22 +18,16 @@ import { Module } from "./common.mjs" import Latex from "./latex.mjs" +import { HistoryInterface, NUMBER, STRING } from "./interface.mjs" class HistoryAPI extends Module { constructor() { super("History", { - historyObj: { - undo: Function, - redo: Function, - clear: Function, - addToHistory: Function, - unserialize: Function, - serialize: Function - }, - themeTextColor: "string", - imageDepth: "number", - fontSize: "number" + historyObj: HistoryInterface, + themeTextColor: STRING, + imageDepth: NUMBER, + fontSize: NUMBER }) // History QML object this.history = null diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/module/interface.mjs b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/module/interface.mjs new file mode 100644 index 0000000..9da3f4d --- /dev/null +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/module/interface.mjs @@ -0,0 +1,130 @@ +/** + * 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 . + */ + +export const NUMBER = 0 +export const STRING = "string" +export const BOOLEAN = true +export const OBJECT = {} +export const FUNCTION = () => { +} + + +export class Interface { + /** + * Checks if the class to check implements the given interface. + * Throws an error if the implementation does not conform to the interface. + * @param {typeof Interface} interface_ + * @param {object} classToCheck + * @return {boolean} + */ + static check_implementation(interface_, classToCheck) { + const properties = new interface_() + const interfaceName = interface_.name + const toCheckName = classToCheck.constructor.name + for(const [property, value] of Object.entries(properties)) + if(property !== "implement") { + console.log(classToCheck[property], value) + if(!classToCheck.hasOwnProperty(property)) + // Check if the property exist + throw new Error(`Property '${property}' (${typeof value}) is present in interface ${interfaceName}, but not in implementation ${toCheckName}.`) + else if((typeof value) !== (typeof classToCheck[property])) + // Compare the types + throw new Error(`Property '${property}' of ${interfaceName} implementation ${toCheckName} is a '${typeof classToCheck[property]}' and not a '${typeof value}'.`) + else if((typeof value) === "object") + // Test type of object. + if(value instanceof Interface) + Interface.check_implementation(value, classToCheck[property]) + else if(value.prototype && !(classToCheck[property] instanceof value)) + throw new Error(`Property '${property}' of ${interfaceName} implementation ${toCheckName} is not '${value.constructor.name}'.`) + } + } + + /** + * Decorator to automatically check if a class conforms to the current interface. + * @param {object} class_ + */ + implement(class_) { + Interface.check_implementation(this, class_) + return class_ + } +} + + +export class SettingsInterface extends Interface { + constructor() { + super() + this.width = NUMBER + this.height = NUMBER + this.xmin = NUMBER + this.ymax = NUMBER + this.xzoom = NUMBER + this.yzoom = NUMBER + this.xaxisstep = STRING + this.yaxisstep = STRING + this.xlabel = STRING + this.ylabel = STRING + this.linewidth = NUMBER + this.textsize = NUMBER + this.logscalex = BOOLEAN + this.showxgrad = BOOLEAN + this.showygrad = BOOLEAN + } +} + +export class CanvasInterface extends SettingsInterface { + constructor() { + super() + this.imageLoaders = OBJECT + /** @type {function(string): CanvasRenderingContext2D} */ + this.getContext = FUNCTION + /** @type {function(rect)} */ + this.markDirty = FUNCTION + /** @type {function(string)} */ + this.loadImage = FUNCTION + /** @type {function()} */ + this.requestPaint = FUNCTION + } +} + +export class RootInterface extends Interface { + constructor() { + super() + this.width = NUMBER + this.height = NUMBER + this.updateObjectsLists = FUNCTION + } +} + +export class DialogInterface extends Interface { + constructor() { + super() + this.show = FUNCTION + } +} + +export class HistoryInterface extends Interface { + constructor() { + super() + this.undo = FUNCTION + this.redo = FUNCTION + this.clear = FUNCTION + this.addToHistory = FUNCTION + this.unserialize = FUNCTION + this.serialize = FUNCTION + } +} \ No newline at end of file diff --git a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/module/io.mjs b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/module/io.mjs index 9a9d893..2a540a7 100644 --- a/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/module/io.mjs +++ b/LogarithmPlotter/qml/eu/ad5001/LogarithmPlotter/js/module/io.mjs @@ -20,55 +20,16 @@ import { Module } from "./common.mjs" import Objects from "./objects.mjs" import History from "./history.mjs" import Canvas from "./canvas.mjs" +import { DialogInterface, FUNCTION, Interface, RootInterface, SettingsInterface } from "./interface.mjs" -/** - * @typedef Settings - * @property {number} width - * @property {number} height - * @property {number} xmin - * @property {number} ymax - * @property {number} xzoom - * @property {number} yzoom - * @property {number} xaxisstep - * @property {number} yaxisstep - * @property {string} xlabel - * @property {string} ylabel - * @property {number} linewidth - * @property {number} textsize - * @property {boolean} logscalex - * @property {boolean} showxgrad - * @property {boolean} showygrad - */ class IOAPI extends Module { constructor() { super("IO", { - root: { - width: "number", - height: "number", - updateObjectsLists: Function, - }, - alert: { - show: Function - }, - settings: { - width: "number", - height: "number", - xmin: "number", - ymax: "number", - xzoom: "number", - yzoom: "number", - xaxisstep: "number", - yaxisstep: "number", - xlabel: "string", - ylabel: "string", - linewidth: "number", - textsize: "number", - logscalex: "boolean", - showxgrad: "boolean", - showygrad: "boolean" - } + alert: DialogInterface, + root: RootInterface, + settings: SettingsInterface }) /** * Path of the currently opened file. Empty if no file is opened. @@ -79,8 +40,8 @@ class IOAPI extends Module { /** * Initializes module with QML elements. - * @param {{width: number, height: number, updateObjectsLists: function()}} root - * @param {Settings} settings + * @param {RootInterface} root + * @param {SettingsInterface} settings * @param {{show: function(string)}} alert */ initialize({ root, settings, alert }) {