Decoupled History from QML

This commit is contained in:
Adsooi 2024-10-11 01:14:52 +02:00
parent 54363b25bc
commit 2dc9234b22
Signed by: Ad5001
GPG key ID: EF45F9C6AFE20160
10 changed files with 239 additions and 191 deletions

View file

@ -54,29 +54,41 @@ export class BaseEventEmitter {
* @param {function(BaseEvent)} eventListener - The function to be called back when the event is emitted.
*/
on(eventType, eventListener) {
if(!this.constructor.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(eventType.includes(" ")) // Listen to several different events with the same listener.
for(const type of eventType.split(" "))
this.on(type, eventListener)
else {
console.log("Listening to", eventType)
if(!this.constructor.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)
}
if(!this.#listeners[eventType].has(eventListener))
this.#listeners[eventType].add(eventListener)
}
/**
* Remvoes a listener from an event that can be emitted by this object.
* Removes 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.
*/
off(eventType, eventListener) {
if(!this.constructor.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(eventType.includes(" ")) { // Unlisten to several different events with the same listener.
let found = false
for(const type of eventType.split(" "))
found ||= this.off(eventType, eventListener)
} else {
if(!this.constructor.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)
}
return this.#listeners[eventType].delete(eventListener)
}
/**

View file

@ -67,6 +67,19 @@ function stringReplaceAll(from, to) {
return this.split(from).join(to)
}
/**
* Returns the value of an element of the array at a given index.
* Accepts negative indexes.
* @this {Array|string}
* @param {number} index
* @return {*}
*/
function arrayAt(index) {
if(typeof index !== "number")
throw new Error(`${index} is not a number`)
return index >= 0 ? this[index] : this[this.length + index]
}
const polyfills = {
2017: [
@ -95,8 +108,8 @@ const polyfills = {
[String.prototype, "replaceAll", stringReplaceAll]
],
2022: [
[Array.prototype, "at", notPolyfilled("Array.prototype.at")],
[String.prototype, "at", notPolyfilled("String.prototype.at")],
[Array.prototype, "at", arrayAt],
[String.prototype, "at", arrayAt],
[Object, "hasOwn", notPolyfilled("Object.hasOwn")]
],
2023: [

View file

@ -17,60 +17,151 @@
*/
import { Module } from "./common.mjs"
import { HistoryInterface, NUMBER, STRING } from "./interface.mjs"
import { HelperInterface, HistoryInterface, NUMBER, STRING } from "./interface.mjs"
import { BaseEvent } from "../events.mjs"
import { Action, Actions } from "../history/index.mjs"
class UpdatedEvent extends BaseEvent {
constructor() {
super("updated")
}
}
class UndoneEvent extends BaseEvent {
constructor(action) {
super("undone")
this.undid = action
}
}
class RedoneEvent extends BaseEvent {
constructor(action) {
super("redone")
this.redid = action
}
}
class HistoryAPI extends Module {
static emits = ["updated", "undone", "redone"]
#helper
constructor() {
super("History", {
historyObj: HistoryInterface,
helper: HelperInterface,
themeTextColor: STRING,
imageDepth: NUMBER,
fontSize: NUMBER
})
// History QML object
this.history = null
/** @type {Action[]} */
this.undoStack = []
/** @type {Action[]} */
this.redoStack = []
this.themeTextColor = "#FF0000"
this.imageDepth = 2
this.fontSize = 28
}
initialize({ historyObj, themeTextColor, imageDepth, fontSize }) {
super.initialize({ historyObj, themeTextColor, imageDepth, fontSize })
this.history = historyObj
/**
* @param {HelperInterface} historyObj
* @param {string} themeTextColor
* @param {number} imageDepth
* @param {number} fontSize
*/
initialize({ helper, themeTextColor, imageDepth, fontSize }) {
super.initialize({ helper, themeTextColor, imageDepth, fontSize })
this.#helper = helper
this.themeTextColor = themeTextColor
this.imageDepth = imageDepth
this.fontSize = fontSize
}
/**
* Undoes the Action at the top of the undo stack and pushes it to the top of the redo stack.
*/
undo() {
if(!this.initialized) throw new Error("Attempting undo before initialize!")
this.history.undo()
if(this.undoStack.length > 0) {
const action = this.undoStack.pop()
action.undo()
this.redoStack.push(action)
this.emit(new UndoneEvent(action))
}
}
/**
* Redoes the Action at the top of the redo stack and pushes it to the top of the undo stack.
*/
redo() {
if(!this.initialized) throw new Error("Attempting redo before initialize!")
this.history.redo()
if(this.redoStack.length > 0) {
const action = this.redoStack.pop()
action.redo()
this.undoStack.push(action)
this.emit(new RedoneEvent(action))
}
}
/**
* Clears both undo and redo stacks completely.
*/
clear() {
if(!this.initialized) throw new Error("Attempting clear before initialize!")
this.history.clear()
this.undoStack = []
this.redoStack = []
this.emit(new UpdatedEvent())
}
/**
* Adds an instance of HistoryLib.Action to history.
* @param action
*/
addToHistory(action) {
if(!this.initialized) throw new Error("Attempting addToHistory before initialize!")
this.history.addToHistory(action)
if(action instanceof Action) {
console.log("Added new entry to history: " + action.getReadableString())
this.undoStack.push(action)
if(this.#helper.getSettingBool("reset_redo_stack"))
this.redoStack = []
this.emit(new UpdatedEvent())
}
}
unserialize(...data) {
/**
* Unserializes both the undo stack and redo stack from serialized content.
* @param {[string, any[]][]} undoSt
* @param {[string, any[]][]} redoSt
*/
unserialize(undoSt, redoSt) {
if(!this.initialized) throw new Error("Attempting unserialize before initialize!")
this.history.unserialize(...data)
this.clear()
for(const [name, args] of undoSt)
this.undoStack.push(
new Actions[name](...args)
)
for(const [name, args] of redoSt)
this.redoStack.push(
new Actions[name](...args)
)
this.emit(new UpdatedEvent())
}
/**
* Serializes history into JSON-able content.
* @return {[[string, any[]], [string, any[]]]}
*/
serialize() {
if(!this.initialized) throw new Error("Attempting serialize before initialize!")
return this.history.serialize()
let undoSt = [], redoSt = [];
for(const action of this.undoStack)
undoSt.push([ action.type(), action.export() ])
for(const action of this.redoStack)
redoSt.push([ action.type(), action.export() ])
return [undoSt, redoSt]
}
}

View file

@ -60,24 +60,6 @@ export class Interface {
}
export class SettingsInterface extends Interface {
width = NUMBER
height = NUMBER
xmin = NUMBER
ymax = NUMBER
xzoom = NUMBER
yzoom = NUMBER
xaxisstep = STRING
yaxisstep = STRING
xlabel = STRING
ylabel = STRING
linewidth = NUMBER
textsize = NUMBER
logscalex = BOOLEAN
showxgrad = BOOLEAN
showygrad = BOOLEAN
}
export class CanvasInterface extends Interface {
imageLoaders = OBJECT
/** @type {function(string): CanvasRenderingContext2D} */

View file

@ -21,7 +21,7 @@ import Objects from "./objects.mjs"
import History from "./history.mjs"
import Canvas from "./canvas.mjs"
import Settings from "./settings.mjs"
import { DialogInterface, RootInterface, SettingsInterface } from "./interface.mjs"
import { DialogInterface, RootInterface } from "./interface.mjs"
class IOAPI extends Module {

View file

@ -72,7 +72,11 @@ class SettingsAPI extends Module {
helper: HelperInterface
})
}
/**
*
* @param {HelperInterface} helper
*/
initialize({ helper }) {
super.initialize({ helper })
// Initialize default values.