Better interfaces!
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
Ad5001 2024-09-24 00:04:11 +02:00
parent 4c1403c983
commit 6a1f01ba1f
Signed by: Ad5001
GPG key ID: EF45F9C6AFE20160
6 changed files with 155 additions and 114 deletions

View file

@ -140,7 +140,7 @@ Canvas {
id: drawingErrorDialog id: drawingErrorDialog
title: qsTranslate("expression", "LogarithmPlotter - Drawing error") title: qsTranslate("expression", "LogarithmPlotter - Drawing error")
text: "" 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) text = qsTranslate("error", "Error while attempting to draw %1 %2:\n%3\n\nUndoing last change.").arg(objType).arg(objName).arg(error)
open() open()
} }

View file

@ -17,60 +17,28 @@
*/ */
import { Module } from "./common.mjs" import { Module } from "./common.mjs"
import { FUNCTION, Interface, CanvasInterface, DialogInterface } from "./interface.mjs"
import { textsup } from "../utils.mjs" import { textsup } from "../utils.mjs"
import { Expression } from "../math/index.mjs" import { Expression } from "../math/index.mjs"
import Latex from "./latex.mjs" import Latex from "./latex.mjs"
import Objects from "./objects.mjs" import Objects from "./objects.mjs"
import History from "./history.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 { class CanvasAPI extends Module {
constructor() { constructor() {
super("Canvas", { super("Canvas", {
canvas: { canvas: CanvasInterface,
width: "number", drawingErrorDialog: DialogInterface
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,
}
}) })
/** @type {Canvas} */ /** @type {CanvasInterface} */
this._canvas = null this._canvas = null
/** @type {CanvasRenderingContext2D} */ /** @type {CanvasRenderingContext2D} */
this._ctx = null this._ctx = null
/** /**
* @type {{showDialog(string, string, string)}} * @type {{show(string, string, string)}}
* @private * @private
*/ */
this._drawingErrorDialog = null this._drawingErrorDialog = null
@ -94,8 +62,8 @@ class CanvasAPI extends Module {
/** /**
* Initialize the module. * Initialize the module.
* @param {Canvas} canvas * @param {CanvasInterface} canvas
* @param {{showDialog(string, string, string)}} drawingErrorDialog * @param {{show(string, string, string)}} drawingErrorDialog
*/ */
initialize({ canvas, drawingErrorDialog }) { initialize({ canvas, drawingErrorDialog }) {
super.initialize({ canvas, drawingErrorDialog }) super.initialize({ canvas, drawingErrorDialog })

View file

@ -16,6 +16,8 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import { Interface } from "./interface.mjs"
/** /**
* Base class for global APIs in runtime. * Base class for global APIs in runtime.
*/ */
@ -24,7 +26,7 @@ export class Module {
/** /**
* *
* @param {string} name - Name of the API * @param {string} name - Name of the API
* @param {Object.<string, (Object.<string, string|object>|string[]|string|object)>} initializationParameters - List of parameters for the initialize function. * @param {Object.<string, (Interface|string)>} initializationParameters - List of parameters for the initialize function.
*/ */
constructor(name, initializationParameters = {}) { constructor(name, initializationParameters = {}) {
console.log(`Loading module ${name}...`) console.log(`Loading module ${name}...`)
@ -43,23 +45,9 @@ export class Module {
for(const [name, value] of Object.entries(this.__initializationParameters)) { for(const [name, value] of Object.entries(this.__initializationParameters)) {
if(!options.hasOwnProperty(name)) if(!options.hasOwnProperty(name))
throw new Error(`Option '${name}' of initialize of module ${this.__name} does not exist.`) throw new Error(`Option '${name}' of initialize of module ${this.__name} does not exist.`)
if(typeof value === "object") { if(typeof value === "function" && value.prototype instanceof Interface)
if(value instanceof Array) Interface.check_implementation(value, options[name])
for(const k in value) else if(typeof value !== typeof options[name])
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)
throw new Error(`Option '${name}' of initialize of module ${this.__name} is not a '${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 this.initialized = true

View file

@ -18,22 +18,16 @@
import { Module } from "./common.mjs" import { Module } from "./common.mjs"
import Latex from "./latex.mjs" import Latex from "./latex.mjs"
import { HistoryInterface, NUMBER, STRING } from "./interface.mjs"
class HistoryAPI extends Module { class HistoryAPI extends Module {
constructor() { constructor() {
super("History", { super("History", {
historyObj: { historyObj: HistoryInterface,
undo: Function, themeTextColor: STRING,
redo: Function, imageDepth: NUMBER,
clear: Function, fontSize: NUMBER
addToHistory: Function,
unserialize: Function,
serialize: Function
},
themeTextColor: "string",
imageDepth: "number",
fontSize: "number"
}) })
// History QML object // History QML object
this.history = null this.history = null

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
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
}
}

View file

@ -20,55 +20,16 @@ import { Module } from "./common.mjs"
import Objects from "./objects.mjs" import Objects from "./objects.mjs"
import History from "./history.mjs" import History from "./history.mjs"
import Canvas from "./canvas.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 { class IOAPI extends Module {
constructor() { constructor() {
super("IO", { super("IO", {
root: { alert: DialogInterface,
width: "number", root: RootInterface,
height: "number", settings: SettingsInterface
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"
}
}) })
/** /**
* Path of the currently opened file. Empty if no file is opened. * 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. * Initializes module with QML elements.
* @param {{width: number, height: number, updateObjectsLists: function()}} root * @param {RootInterface} root
* @param {Settings} settings * @param {SettingsInterface} settings
* @param {{show: function(string)}} alert * @param {{show: function(string)}} alert
*/ */
initialize({ root, settings, alert }) { initialize({ root, settings, alert }) {