Decoupled History from QML
This commit is contained in:
parent
54363b25bc
commit
2dc9234b22
10 changed files with 239 additions and 191 deletions
|
@ -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)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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: [
|
||||
|
|
|
@ -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]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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} */
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -72,7 +72,11 @@ class SettingsAPI extends Module {
|
|||
helper: HelperInterface
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {HelperInterface} helper
|
||||
*/
|
||||
initialize({ helper }) {
|
||||
super.initialize({ helper })
|
||||
// Initialize default values.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue