mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-19 17:43:50 +00:00
Enable proper event hooking
This commit is contained in:
parent
cb7e9e6882
commit
8650ae00b1
14 changed files with 219 additions and 67 deletions
|
|
@ -1,8 +1,17 @@
|
|||
import JSZip from "jszip";
|
||||
// This module is imported early here in order to avoid a circular dependency.
|
||||
import { classResolver } from "./skin/resolver";
|
||||
import SkinParser from "./skin/parse";
|
||||
|
||||
function hack() {
|
||||
// Without this Snowpack will try to treeshake out resolver causing a circular
|
||||
// dependency.
|
||||
classResolver("A funny joke about why this is needed.");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const response = await fetch("assets/CornerAmp_Redux.wal");
|
||||
// const response = await fetch("assets/Default_winamp3_build499.wal");
|
||||
const data = await response.blob();
|
||||
const zip = await JSZip.loadAsync(data);
|
||||
|
||||
|
|
@ -22,6 +31,9 @@ async function main() {
|
|||
|
||||
document.body.appendChild(node);
|
||||
setInterval(() => {
|
||||
if (window.pause) {
|
||||
return;
|
||||
}
|
||||
document.body.removeChild(node);
|
||||
node = document.createElement("div");
|
||||
for (const container of parser._containers) {
|
||||
|
|
|
|||
|
|
@ -5,9 +5,11 @@ import { ParsedMaki, Command, Method } from "./parser";
|
|||
export function interpret(
|
||||
start: number,
|
||||
program: ParsedMaki,
|
||||
stack: Variable[],
|
||||
classResolver: (guid: string) => any
|
||||
) {
|
||||
const interpreter = new Interpreter(program, classResolver);
|
||||
interpreter.stack = stack;
|
||||
return interpreter.interpret(start);
|
||||
}
|
||||
|
||||
|
|
@ -18,7 +20,7 @@ class Interpreter {
|
|||
variables: Variable[];
|
||||
methods: Method[];
|
||||
commands: Command[];
|
||||
debug: boolean;
|
||||
debug: boolean = false;
|
||||
classResolver: (guid: string) => any;
|
||||
constructor(program: ParsedMaki, classResolver: (guid: string) => any) {
|
||||
const { commands, methods, variables, classes } = program;
|
||||
|
|
@ -30,7 +32,6 @@ class Interpreter {
|
|||
|
||||
this.stack = [];
|
||||
this.callStack = [];
|
||||
this.debug = false;
|
||||
}
|
||||
|
||||
interpret(start: number) {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { assert, assume } from "../utils";
|
|||
import { COMMANDS } from "./constants";
|
||||
import { DataType, Variable } from "./v";
|
||||
import MakiFile from "./MakiFile";
|
||||
import { getMethod, getReturnType } from "./objects";
|
||||
import { getReturnType } from "./objects";
|
||||
|
||||
export type Command = {
|
||||
opcode: number;
|
||||
|
|
@ -27,6 +27,8 @@ export type ParsedMaki = {
|
|||
export type Binding = {
|
||||
commandOffset: number;
|
||||
methodOffset: number;
|
||||
variableOffset: number;
|
||||
binaryOffset: number;
|
||||
};
|
||||
|
||||
const MAGIC = "FG";
|
||||
|
|
@ -70,18 +72,24 @@ export function parse(data: ArrayBuffer): ParsedMaki {
|
|||
}
|
||||
});
|
||||
|
||||
const resolvedBindings = bindings.map((binding): Binding => {
|
||||
return Object.assign({}, binding, {
|
||||
commandOffset: offsetToCommand[binding.binaryOffset],
|
||||
});
|
||||
});
|
||||
|
||||
const resolvedCommands = commands.map((command): Command => {
|
||||
if (command.argType === "COMMAND_OFFSET") {
|
||||
return Object.assign({}, command, { arg: offsetToCommand[command.arg] });
|
||||
const resolvedBindings = bindings.map(
|
||||
(binding): Binding => {
|
||||
return Object.assign({}, binding, {
|
||||
commandOffset: offsetToCommand[binding.binaryOffset],
|
||||
});
|
||||
}
|
||||
return command;
|
||||
});
|
||||
);
|
||||
|
||||
const resolvedCommands = commands.map(
|
||||
(command): Command => {
|
||||
if (command.argType === "COMMAND_OFFSET") {
|
||||
return Object.assign({}, command, {
|
||||
arg: offsetToCommand[command.arg],
|
||||
});
|
||||
}
|
||||
return command;
|
||||
}
|
||||
);
|
||||
return {
|
||||
classes,
|
||||
methods,
|
||||
|
|
@ -150,7 +158,7 @@ function readMethods(makiFile: MakiFile, classes: string[]): Method[] {
|
|||
const typeOffset = classCode & 0xff;
|
||||
// This is probably the second half of a uint32
|
||||
makiFile.readUInt16LE();
|
||||
const name = makiFile.readString();
|
||||
const name = makiFile.readString().toLowerCase();
|
||||
|
||||
const className = classes[typeOffset];
|
||||
|
||||
|
|
@ -238,7 +246,7 @@ function readConstants({ makiFile, variables }) {
|
|||
}
|
||||
}
|
||||
|
||||
function readBindings(makiFile) {
|
||||
function readBindings(makiFile: MakiFile): Binding[] {
|
||||
let count = makiFile.readUInt32LE();
|
||||
const bindings = [];
|
||||
while (count--) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import GuiObj from "../skin/GuiObj";
|
||||
import BaseObject from "../skin/BaseObject";
|
||||
|
||||
export type Variable =
|
||||
| {
|
||||
|
|
@ -15,7 +15,7 @@ export type Variable =
|
|||
}
|
||||
| {
|
||||
type: "OBJECT";
|
||||
value: GuiObj;
|
||||
value: BaseObject;
|
||||
}
|
||||
| {
|
||||
type: "NULL";
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
import { SkinContext } from "../types";
|
||||
import { toBool } from "../utils";
|
||||
import Group from "./Group";
|
||||
import Layer from "./Layer";
|
||||
import Layout from "./Layout";
|
||||
import XmlObj from "./XmlObj";
|
||||
|
||||
|
|
@ -74,6 +72,9 @@ export default class Container extends XmlObj {
|
|||
|
||||
getDebugDom(): HTMLDivElement {
|
||||
const div = window.document.createElement("div");
|
||||
div.setAttribute("data-xml-id", this.getId());
|
||||
div.setAttribute("data-obj-name", "Container");
|
||||
|
||||
if (this._defaultVisible && this._activeLayout) {
|
||||
div.appendChild(this._activeLayout.getDebugDom());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ export default class Group extends GuiObj {
|
|||
|
||||
getDebugDom(): HTMLDivElement {
|
||||
const div = super.getDebugDom();
|
||||
div.setAttribute("data-obj-name", "Group");
|
||||
div.style.height = Utils.px(this._maximumHeight);
|
||||
div.style.width = Utils.px(this._maximumWidth);
|
||||
if (this._background != null && this._drawBackground) {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { SkinContext } from "../types";
|
||||
import * as Utils from "../utils";
|
||||
import { VM } from "./VM";
|
||||
import XmlObj from "./XmlObj";
|
||||
|
||||
// http://wiki.winamp.com/wiki/XML_GUI_Objects#GuiObject_.28Global_params.29
|
||||
|
|
@ -112,8 +113,65 @@ export default class GuiObj extends XmlObj {
|
|||
this._dirty = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hookable. Event happens when the left mouse
|
||||
* button was previously down and is now up.
|
||||
*
|
||||
* @param x The X position in the screen where the cursor was when the event was triggered.
|
||||
* @param y The Y position in the screen where the cursor was when the event was triggered.
|
||||
*/
|
||||
onLeftButtonUp(x: number, y: number) {
|
||||
VM.dispatch(this, "onleftbuttonup", [
|
||||
{ type: "INT", value: x },
|
||||
{ type: "INT", value: y },
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hookable. Event happens when the left mouse button
|
||||
* is pressed.
|
||||
*
|
||||
* @param x The X position in the screen where the cursor was when the event was triggered.
|
||||
* @param y The Y position in the screen where the cursor was when the event was triggered.
|
||||
*/
|
||||
onLeftButtonDown(x: number, y: number) {
|
||||
VM.dispatch(this, "onleftbuttondown", [
|
||||
{ type: "INT", value: x },
|
||||
{ type: "INT", value: y },
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hookable. Event happens when the right mouse button
|
||||
* was previously down and is now up.
|
||||
*
|
||||
* @param x The X position in the screen where the cursor was when the event was triggered.
|
||||
* @param y The Y position in the screen where the cursor was when the event was triggered.
|
||||
*/
|
||||
onRightButtonUp(x: number, y: number) {
|
||||
VM.dispatch(this, "onrightbuttonup", [
|
||||
{ type: "INT", value: x },
|
||||
{ type: "INT", value: y },
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hookable. Event happens when the right mouse button
|
||||
* is pressed.
|
||||
*
|
||||
* @param x The X position in the screen where the cursor was when the event was triggered.
|
||||
* @param y The Y position in the screen where the cursor was when the event was triggered.
|
||||
*/
|
||||
onRightButtonDown(x: number, y: number) {
|
||||
VM.dispatch(this, "onrightbuttondown", [
|
||||
{ type: "INT", value: x },
|
||||
{ type: "INT", value: y },
|
||||
]);
|
||||
}
|
||||
|
||||
getDebugDom(): HTMLDivElement {
|
||||
const div = window.document.createElement("div");
|
||||
div.setAttribute("data-id", this.getId());
|
||||
div.style.display = this._visible ? "inline-block" : "none";
|
||||
div.style.position = "absolute";
|
||||
if (this._x) {
|
||||
|
|
@ -128,6 +186,13 @@ export default class GuiObj extends XmlObj {
|
|||
if (this._height) {
|
||||
div.style.height = Utils.px(this._height);
|
||||
}
|
||||
div.addEventListener("mouseup", (e) => {
|
||||
this.onLeftButtonUp(e.clientX, e.clientX);
|
||||
});
|
||||
|
||||
div.addEventListener("mousedown", (e) => {
|
||||
this.onLeftButtonDown(e.clientX, e.clientX);
|
||||
});
|
||||
return div;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ export default class Layer extends GuiObj {
|
|||
|
||||
getDebugDom(): HTMLDivElement {
|
||||
const div = super.getDebugDom();
|
||||
div.setAttribute("data-obj-name", "Layer");
|
||||
if (this._image != null) {
|
||||
const bitmap = UI_ROOT.getBitmap(this._image);
|
||||
div.style.background = bitmap.getBackgrondCSSAttribute();
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import Group from "./Group";
|
||||
import * as Utils from "../utils";
|
||||
import Container from "./Container";
|
||||
import Layer from "./Layer";
|
||||
|
||||
// > A layout is a special kind of group, which shown inside a container. Each
|
||||
// > layout represents an appearance for that window. Layouts give you the ability
|
||||
|
|
@ -31,4 +30,10 @@ export default class Layout extends Group {
|
|||
setParent(container: Container) {
|
||||
this._parent = container;
|
||||
}
|
||||
|
||||
getDebugDom(): HTMLDivElement {
|
||||
const div = super.getDebugDom();
|
||||
div.setAttribute("data-obj-name", "Layout");
|
||||
return div;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,19 @@
|
|||
import { interpret } from "../maki/interpreter";
|
||||
import { getClass } from "../maki/objects";
|
||||
import { ParsedMaki } from "../maki/parser";
|
||||
import { SkinContext } from "../types";
|
||||
import Button from "./Button";
|
||||
import BaseObject from "./BaseObject";
|
||||
import Container from "./Container";
|
||||
|
||||
import Group from "./Group";
|
||||
import Layer from "./Layer";
|
||||
import Layout from "./Layout";
|
||||
import PopupMenu from "./PopupMenu";
|
||||
import Status from "./Status";
|
||||
import Text from "./Text";
|
||||
import ToggleButton from "./ToggleButton";
|
||||
import { VM } from "./VM";
|
||||
|
||||
export default class SystemObject {
|
||||
export default class SystemObject extends BaseObject {
|
||||
_parentGroup: Group;
|
||||
_parsedScript: ParsedMaki;
|
||||
_context: SkinContext;
|
||||
|
||||
constructor(parsedScript: ParsedMaki) {
|
||||
super();
|
||||
this._parsedScript = parsedScript;
|
||||
}
|
||||
|
||||
|
|
@ -30,15 +25,9 @@ export default class SystemObject {
|
|||
throw new Error("First variable was not SystemObject.");
|
||||
}
|
||||
initialVariable.value = this;
|
||||
// TODO: How should we setup bindings?
|
||||
// console.log(this._parsedScript.bindings);
|
||||
interpret(0, this._parsedScript, classResover);
|
||||
|
||||
interpret(
|
||||
this._parsedScript.bindings[0].commandOffset,
|
||||
this._parsedScript,
|
||||
classResover
|
||||
);
|
||||
VM.addScript(this._parsedScript);
|
||||
VM.dispatch(this, "onscriptloaded");
|
||||
}
|
||||
|
||||
setParentGroup(group: Group) {
|
||||
|
|
@ -166,33 +155,6 @@ export default class SystemObject {
|
|||
}
|
||||
}
|
||||
|
||||
function classResover(guid: string): any {
|
||||
switch (guid) {
|
||||
case "d6f50f6449b793fa66baf193983eaeef":
|
||||
return SystemObject;
|
||||
case "e90dc47b4ae7840d0b042cb0fcf775d2":
|
||||
return Container;
|
||||
case "60906d4e482e537e94cc04b072568861":
|
||||
return Layout;
|
||||
case "5ab9fa1545579a7d5765c8aba97cc6a6":
|
||||
return Layer;
|
||||
case "f4787af44ef7b2bb4be7fb9c8da8bea9":
|
||||
return PopupMenu;
|
||||
case "698eddcd4fec8f1e44f9129b45ff09f9":
|
||||
return Button;
|
||||
case "b4dccfff4bcc81fe0f721b96ff0fbed5":
|
||||
return ToggleButton;
|
||||
case "efaa867241fa310ea985dcb74bcb5b52":
|
||||
return Text;
|
||||
case "0f08c9404b23af39c4b8f38059bb7e8f":
|
||||
return Status;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unresolvable class "${getClass(guid).name}" (guid: ${guid})`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function dumpScriptDebug(script: ParsedMaki) {
|
||||
for (const [i, binding] of script.bindings.entries()) {
|
||||
const method = script.methods[binding.methodOffset];
|
||||
|
|
|
|||
37
packages/webamp-modern-2/src/skin/VM.ts
Normal file
37
packages/webamp-modern-2/src/skin/VM.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import { interpret } from "../maki/interpreter";
|
||||
import { ParsedMaki } from "../maki/parser";
|
||||
import { Variable } from "../maki/v";
|
||||
import BaseObject from "./BaseObject";
|
||||
|
||||
import { classResolver } from "./resolver";
|
||||
|
||||
class Vm {
|
||||
_scripts: ParsedMaki[] = [];
|
||||
// This could easily become performance sensitive. We could make this more
|
||||
// performant by normalizing some of these things when scripts are added.
|
||||
dispatch(object: BaseObject, event: string, args: Variable[] = []) {
|
||||
for (const [scriptId, script] of this._scripts.entries()) {
|
||||
for (const binding of script.bindings) {
|
||||
if (
|
||||
script.methods[binding.methodOffset].name === event &&
|
||||
script.variables[binding.variableOffset].value === object
|
||||
) {
|
||||
this.interpret(scriptId, binding.commandOffset, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
addScript(maki: ParsedMaki): number {
|
||||
const index = this._scripts.length;
|
||||
this._scripts.push(maki);
|
||||
return index;
|
||||
}
|
||||
|
||||
interpret(scriptId: number, commandOffset: number, args: Variable[]) {
|
||||
const script = this._scripts[scriptId];
|
||||
interpret(commandOffset, script, args, classResolver);
|
||||
}
|
||||
}
|
||||
|
||||
export const VM = new Vm();
|
||||
|
|
@ -1,4 +1,6 @@
|
|||
export default class XmlObj {
|
||||
import BaseObject from "./BaseObject";
|
||||
|
||||
export default class XmlObj extends BaseObject {
|
||||
setXmlAttributes(attributes: { [attrName: string]: string }) {
|
||||
for (const [key, value] of Object.entries(attributes)) {
|
||||
this.setXmlAttr(key, value);
|
||||
|
|
|
|||
|
|
@ -55,6 +55,8 @@ export default class SkinParser {
|
|||
switch (node.name.toLowerCase()) {
|
||||
case "wasabixml":
|
||||
return this.wasabiXml(node);
|
||||
case "winampabstractionlayer":
|
||||
return this.winampAbstractionLayer(node);
|
||||
case "include":
|
||||
return this.include(node);
|
||||
case "skininfo":
|
||||
|
|
@ -63,6 +65,8 @@ export default class SkinParser {
|
|||
return this.elements(node);
|
||||
case "bitmap":
|
||||
return this.bitmap(node);
|
||||
case "bitmapfont":
|
||||
return this.bitmapFont(node);
|
||||
case "color":
|
||||
return this.color(node);
|
||||
case "groupdef":
|
||||
|
|
@ -130,6 +134,10 @@ export default class SkinParser {
|
|||
await this.traverseChildren(node);
|
||||
}
|
||||
|
||||
async winampAbstractionLayer(node: XmlElement) {
|
||||
await this.traverseChildren(node);
|
||||
}
|
||||
|
||||
async elements(node: XmlElement) {
|
||||
await this.traverseChildren(node);
|
||||
}
|
||||
|
|
@ -153,6 +161,13 @@ export default class SkinParser {
|
|||
UI_ROOT.addBitmap(bitmap);
|
||||
}
|
||||
|
||||
async bitmapFont(node: XmlElement) {
|
||||
assume(
|
||||
node.children.length === 0,
|
||||
"Unexpected children in <bitmapFont> XML node."
|
||||
);
|
||||
}
|
||||
|
||||
async text(node: XmlElement) {
|
||||
assume(
|
||||
node.children.length === 0,
|
||||
|
|
@ -416,7 +431,10 @@ export default class SkinParser {
|
|||
|
||||
const path = [...this._path, fileName].join("/");
|
||||
const zipFile = this.getCaseInsensitiveFile(path);
|
||||
assert(zipFile != null, `Zip file not found for ${file}`);
|
||||
if (zipFile == null) {
|
||||
console.warn(`Zip file not found: ${file}`);
|
||||
return;
|
||||
}
|
||||
const includedXml = await zipFile.async("string");
|
||||
|
||||
// Note: Included files don't have a single root node, so we add a synthetic one.
|
||||
|
|
|
|||
39
packages/webamp-modern-2/src/skin/resolver.ts
Normal file
39
packages/webamp-modern-2/src/skin/resolver.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { getClass } from "../maki/objects";
|
||||
import Button from "./Button";
|
||||
import SystemObject from "./SystemObject";
|
||||
import Container from "./Container";
|
||||
import Layout from "./Layout";
|
||||
import Layer from "./Layer";
|
||||
import PopupMenu from "./PopupMenu";
|
||||
import ToggleButton from "./ToggleButton";
|
||||
import Status from "./Status";
|
||||
import Text from "./Text";
|
||||
|
||||
// TODO: We could write a test using the data in object.ts which confirms that
|
||||
// this is complete.
|
||||
export function classResolver(guid: string): any {
|
||||
switch (guid) {
|
||||
case "d6f50f6449b793fa66baf193983eaeef":
|
||||
return SystemObject;
|
||||
case "e90dc47b4ae7840d0b042cb0fcf775d2":
|
||||
return Container;
|
||||
case "60906d4e482e537e94cc04b072568861":
|
||||
return Layout;
|
||||
case "5ab9fa1545579a7d5765c8aba97cc6a6":
|
||||
return Layer;
|
||||
case "f4787af44ef7b2bb4be7fb9c8da8bea9":
|
||||
return PopupMenu;
|
||||
case "698eddcd4fec8f1e44f9129b45ff09f9":
|
||||
return Button;
|
||||
case "b4dccfff4bcc81fe0f721b96ff0fbed5":
|
||||
return ToggleButton;
|
||||
case "efaa867241fa310ea985dcb74bcb5b52":
|
||||
return Text;
|
||||
case "0f08c9404b23af39c4b8f38059bb7e8f":
|
||||
return Status;
|
||||
default:
|
||||
throw new Error(
|
||||
`Unresolvable class "${getClass(guid).name}" (guid: ${guid})`
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue