diff --git a/packages/webamp-modern/src/UIRoot.ts b/packages/webamp-modern/src/UIRoot.ts index aced5787da..e637fe9d6b 100644 --- a/packages/webamp-modern/src/UIRoot.ts +++ b/packages/webamp-modern/src/UIRoot.ts @@ -28,10 +28,24 @@ import Config from "./skin/makiClasses/Config"; import WinampConfig from "./skin/makiClasses/WinampConfig"; import Avs from "./skin/makiClasses/Avs"; +import { getWa5Popup } from "./skin/makiClasses/menuWa5"; + import { SkinEngineClass } from "./skin/SkinEngine"; -import { FileExtractor } from "./skin/FileExtractor"; +import { + FileExtractor, + PathFileExtractor, + ZipFileExtractor, +} from "./skin/FileExtractor"; import Application from "./skin/makiClasses/Application"; +import { + getSkinEngineClass, + getSkinEngineClassByContent, + SkinEngine, +} from "./skin/SkinEngine"; + +export type Skin = string | { name: string; url: string }; + export class UIRoot { _id: string; _application: Application; @@ -40,6 +54,7 @@ export class UIRoot { _winampConfig: WinampConfig; _div: HTMLDivElement = document.createElement("div"); + _mousePos: { x: number; y: number } = { x: 0, y: 0 }; _imageManager: ImageManager; // Just a temporary place to stash things _bitmaps: { [id: string]: Bitmap } = {}; @@ -62,6 +77,8 @@ export class UIRoot { _xFades: GroupXFade[] = []; _input: HTMLInputElement = document.createElement("input"); _skinInfo: { [key: string]: string } = {}; + _skin: Skin = { name: "", url: "" }; + _skins: Skin[] = []; _skinEngineClass: SkinEngineClass; _eventListener: Emitter = new Emitter(); _additionalCss: string[] = []; @@ -89,6 +106,7 @@ export class UIRoot { this._winampConfig = new WinampConfig(this); this.playlist = new PlEdit(this); // must be after _config. this.vm = new Vm(this); + this.setlistenMouseMove(true); } getId(): string { @@ -516,7 +534,11 @@ export class UIRoot { cssEl.textContent = cssRules.join("\n"); } - dispatch(action: string, param: string | null, actionTarget: string | null) { + dispatch( + action: string, + param?: string | null, + actionTarget?: string | null + ) { switch (action.toLowerCase()) { case "play": this.audio.play(); @@ -556,12 +578,42 @@ export class UIRoot { case "close": this.closeContainer(); break; + + case "menu": + getWa5Popup(param, this).popatmouse(); + break; + + case "controlmenu": + getWa5Popup("ControlMenu", this).popatmouse(); + break; + case "sysmenu": + getWa5Popup("Main", this).popatmouse(); + break; + case "pe_add": + getWa5Popup("Add", this).popatmouse(); + break; + case "pe_rem": + getWa5Popup("Remove", this).popatmouse(); + break; + case "pe_sel": + getWa5Popup("Select", this).popatmouse(); + break; + case "pe_misc": + getWa5Popup("MiscOpt", this).popatmouse(); + break; + case "pe_list": + getWa5Popup("Playlist", this).popatmouse(); + break; default: assume(false, `Unknown global action: ${action}`); } } - getActionState(action: string, param: string, actionTarget: string): boolean { + getActionState( + action: string, + param: string, + actionTarget: string = "" + ): boolean { if (action != null) { switch (action.toLowerCase()) { case "eq_toggle": @@ -653,6 +705,20 @@ export class UIRoot { } } + setlistenMouseMove(listen: boolean) { + const update = (e: MouseEvent) => { + this._mousePos = { + // https://stackoverflow.com/questions/6073505/what-is-the-difference-between-screenx-y-clientx-y-and-pagex-y + x: e.pageX, + y: e.pageY, + }; + }; + window.document[`${listen ? "add" : "remove"}EventListener`]( + "mousemove", + update + ); + } + //? Zip things ======================== /* because maki need to load a groupdef outside init() */ _zip: JSZip; @@ -700,69 +766,6 @@ export class UIRoot { return await this._fileExtractor.getFileAsBlob(filePath); } - // async getFileAsString(filePath: string): Promise { - // if (this._preferZip) { - // return await this.getFileAsStringZip(filePath); - // } else { - // return await this.getFileAsStringPath(filePath); - // } - // } - // async getFileAsBytes(filePath: string): Promise { - // if (this._preferZip) { - // return await this.getFileAsBytesZip(filePath); - // } else { - // return await this.getFileAsBytesPath(filePath); - // } - // } - // async getFileAsBlob(filePath: string): Promise { - // if (this._preferZip) { - // return await this.getFileAsBlobZip(filePath); - // } else { - // return await this.getFileAsBlobPath(filePath); - // } - // } - - // async getFileAsStringZip(filePath: string): Promise { - // if (!filePath) return null; - // const zipObj = getCaseInsensitiveFile(this._zip, filePath); - // if (!zipObj) return null; - // return await zipObj.async("text"); - // } - - // async getFileAsBytesZip(filePath: string): Promise { - // if (!filePath) return null; - // const zipObj = getCaseInsensitiveFile(this._zip, filePath); - // if (!zipObj) return null; - // return await zipObj.async("arraybuffer"); - // } - - // async getFileAsBlobZip(filePath: string): Promise { - // if (!filePath) return null; - // const zipObj = getCaseInsensitiveFile(this._zip, filePath); - // if (!zipObj) return null; - // return await zipObj.async("blob"); - // } - - // async getFileAsStringPath(filePath: string): Promise { - // const response = await fetch(this._skinPath + filePath); - // return await response.text(); - // } - - // async getFileAsBytesPath(filePath: string): Promise { - // const response = await fetch(this._skinPath + filePath); - // return await response.arrayBuffer(); - // } - - // async getFileAsBlobPath(filePath: string): Promise { - // const response = await fetch(this._skinPath + filePath); - // return await response.blob(); - // } - - // getFileIsExist(filePath: string): boolean { - // const zipObj = getCaseInsensitiveFile(this._zip, filePath); - // return !!zipObj; - // } - //? System things ======================== /* because maki need to be run if not inside any Group @init() */ addSystemObject(systemObj: SystemObject) { @@ -782,7 +785,14 @@ export class UIRoot { } setSkinInfo(skinInfo: { [key: string]: string }) { - this._skinInfo = skinInfo; + const url = this._skinInfo.url; + this._skinInfo = { ...skinInfo, url }; + } + setSkinUrl(url: string) { + this._skinInfo.url = url; + } + getSkinUrl() { + return this._skinInfo.url; } getSkinInfo(): { [key: string]: string } { return this._skinInfo; @@ -791,6 +801,103 @@ export class UIRoot { return this.getSkinInfo()["name"]; } + // THIS IS A BIG THING, MOVED HERE FROM AN AGNOSTIC SKIN ENGINES + async switchSkin(skin: Skin) { + //* getting skin engine is complicated: + //* SkinEngine is not yet instanciated during looking for a skinEngine. + //* If file extension is know then we loop for registered Engines + //* But sometime (if its a `.zip` or a path `/`), we need to detect by + //* if a file exist, with a name is expected by skinEngine + + const parentDiv = this.getRootDiv().parentElement; + this.reset(); + // this._parent.appendChild(this._uiRoot.getRootDiv()); + parentDiv.appendChild(this.getRootDiv()); + + const name = typeof skin === "string" ? skin : skin.name; + const skinPath = typeof skin === "string" ? skin : skin.url; + this.setSkinUrl(skinPath); + + let skinFetched = false; + let SkinEngineClass = null; + + //? usually the file extension is explicitly for SkinEngine. eg: `.wal` + let SkinEngineClasses = await getSkinEngineClass(skinPath); + + //? when file extension is ambiguous eg. `.zip`, several + //? skinEngines are supporting, but only one is actually working with. + //? lets detect: + if (SkinEngineClasses.length > 1) { + await this._loadSkinPathToUiroot(skinPath, null); + skinFetched = true; + SkinEngineClass = await getSkinEngineClassByContent( + SkinEngineClasses, + skinPath, + this + ); + } else { + SkinEngineClass = SkinEngineClasses[0]; + } + if (SkinEngineClass == null) { + throw new Error(`Skin not supported`); + } + + //? success found a skin-engine + this.SkinEngineClass = SkinEngineClass; + const parser: SkinEngine = new SkinEngineClass(this); + if (!skinFetched) await this._loadSkinPathToUiroot(skinPath, parser); + // await parser.parseSkin(); + await parser.buildUI(); + + // loadSkin(this._parent, skinPath); + } + + /** + * Time to load the skin file + * @param skinPath url string + * @param uiRoot + * @param skinEngine An instance of SkinEngine + */ + private async _loadSkinPathToUiroot( + skinPath: string, + skinEngine: SkinEngine + ) { + let response: Response; + let fileExtractor: FileExtractor; + //? pick one of correct fileExtractor + + if (skinPath.endsWith("/")) { + fileExtractor = new PathFileExtractor(); + } else { + response = await fetch(skinPath); + if (response.status == 404) { + throw new Error(`Skin does not exist`); + } + if (skinEngine != null) { + fileExtractor = skinEngine.getFileExtractor(); + } + } + if (fileExtractor == null) { + if (response.headers.get("content-type").startsWith("application/")) { + fileExtractor = new ZipFileExtractor(); + } else { + fileExtractor = new PathFileExtractor(); + } + } + + await fileExtractor.prepare(skinPath, response); + // const skinZipBlob = await response.blob(); + + // const zip = await JSZip.loadAsync(skinZipBlob); + // uiRoot.setZip(zip); + // } else { + // uiRoot.setZip(null); + // const slash = skinPath.endsWith("/") ? "" : "/"; + // uiRoot.setSkinDir(skinPath + slash); + // } + this.setFileExtractor(fileExtractor); + } + set SkinEngineClass(Engine: SkinEngineClass) { this._skinEngineClass = Engine; } diff --git a/packages/webamp-modern/src/css/button.css b/packages/webamp-modern/src/css/button.css index b0e91d5bf5..9b60ebf2f8 100644 --- a/packages/webamp-modern/src/css/button.css +++ b/packages/webamp-modern/src/css/button.css @@ -13,4 +13,13 @@ button.wasabi { button.wasabi:active { border-image-source: var(--bitmap-studio-button-pressed); -} \ No newline at end of file +} + +button.center_image::before { + content: ''; + position: absolute; + inset: 0; + background-image: var(--background-image); + background-repeat: no-repeat; + background-position: center center; +} diff --git a/packages/webamp-modern/src/css/demo.css b/packages/webamp-modern/src/css/demo.css index 5d5c2193ce..16285927b7 100644 --- a/packages/webamp-modern/src/css/demo.css +++ b/packages/webamp-modern/src/css/demo.css @@ -1,7 +1,7 @@ body { margin: 0; background-color: rgb(58, 110, 165); - background-image: url(img/wallpaper.png); + /* background-image: url(img/wallpaper.png); */ font-family: Arial, Helvetica, sans-serif; } #experimental { diff --git a/packages/webamp-modern/src/css/elements.css b/packages/webamp-modern/src/css/elements.css index 11dfd445db..c32c7f9f5e 100644 --- a/packages/webamp-modern/src/css/elements.css +++ b/packages/webamp-modern/src/css/elements.css @@ -114,6 +114,16 @@ text wrap { font-family: monospace; white-space: pre; } +text wrap { + margin-left: 2px; +} +text wrap[font="TrueType"] { + /* needed for titlebar */ + font-size: 10.5px; + line-height: 10px; + vertical-align: var(--valign, center); + text-align: var(--align, center); +} text wrap[font="BitmapFont"] { display: flex; white-space: nowrap; @@ -121,7 +131,6 @@ text wrap[font="BitmapFont"] { /* align-items: center; */ align-items: var(--valign, center); justify-content: var(--align, center); - margin-left: 2px; } text span { user-select: none; @@ -148,6 +157,11 @@ menu { padding: 0; list-style: none; } +.popup hr { + margin-block-start: 3px; + margin-block-end: 3px; + border-bottom: none; +} /* frame2 { box-shadow: inset 0 0 5px red; } */ @@ -264,6 +278,7 @@ container:not(:active):not(container:focus-within) .webamp--img.inactivable { } .resizing { + position:fixed; border: 1px solid blue; background-color: rgba(74, 74, 251, 0.205); z-index: 1000; @@ -311,6 +326,7 @@ menu > .popup{ width: auto; display: inline-block; box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15); + font-size: 10.5px; } ul.popup-menu-container li { display: flex; @@ -322,7 +338,7 @@ ul.popup-menu-container li > span { padding: 4px 0; } ul.popup-menu-container li:hover > span { - background: blue; + background: #316ac5; color: white; } @@ -341,6 +357,7 @@ ul.popup-menu-container li:hover > span { min-width: 15px; text-align: center; font-size: smaller; + line-height: 1; } /* nested popup */ @@ -350,4 +367,18 @@ ul.popup-menu-container li:hover > span { } .popup-menu-container > li:hover > .popup-menu-container{ display: unset; -} \ No newline at end of file +} + +/* COMPONENT BUCKET */ +componentbucket[id="component list"] wrapper button { + width: 44px; + height: 34px; + /* border: none; */ + /* border: 2px solid fuchsia; */ + margin: 0 1px; + position: unset; + display: inline-block; + /* left: unset; */ + /* top: unset; */ + /* background: red; */ +} diff --git a/packages/webamp-modern/src/index.ts b/packages/webamp-modern/src/index.ts index f91812bf15..30e5cf503f 100644 --- a/packages/webamp-modern/src/index.ts +++ b/packages/webamp-modern/src/index.ts @@ -109,14 +109,22 @@ async function initializeSkinListMenu() { // MODERN { filename: "[Winamp] default", download_url: "" }, { filename: "[Winamp] MMD3", download_url: "assets/skins/MMD3.wal" }, - { filename: "[Winamp] MMD3+Thinger", download_url: "assets/skins/MMD3+Thinger/" }, + { + filename: "[Winamp] MMD3+Thinger", + download_url: "assets/skins/MMD3+Thinger/", + }, // { filename: "[Folder] MMD3", download_url: "assets/skins/extracted/MMD3/" }, { filename: "[Winamp] BigBento", download_url: "assets/skins/BigBento/" }, - { filename: "CornerAmp_Redux", download_url: "assets/skins/CornerAmp_Redux.wal" }, + { + filename: "CornerAmp_Redux", + download_url: "assets/skins/CornerAmp_Redux.wal", + }, - // CLASSIC - { filename: "[Winamp Classic]", download_url: "assets/skins/base-2.91.wsz" }, + { + filename: "[Winamp Classic]", + download_url: "assets/skins/base-2.91.wsz", + }, { filename: "[Winamp Classic] MacOSXAqua1-5", download_url: "assets/skins/MacOSXAqua1-5.698dd4ab.wsz", @@ -131,7 +139,10 @@ async function initializeSkinListMenu() { filename: "[wmp] Quicksilver WindowsMediaPlayer!", download_url: "assets/skins/Quicksilver.wmz", }, - { filename: "[wmp] Windows XP", download_url: "assets/skins/Windows-XP.wmz" }, + { + filename: "[wmp] Windows XP", + download_url: "assets/skins/Windows-XP.wmz", + }, { filename: "[wmp] Famous Headspace", download_url: "assets/skins/Headspace.wmz", @@ -156,7 +167,10 @@ async function initializeSkinListMenu() { }, // K-JOFOL - { filename: "[K-Jofol] Default", download_url: "assets/skins/Default.kjofol" }, + { + filename: "[K-Jofol] Default", + download_url: "assets/skins/Default.kjofol", + }, { filename: "[K-Jofol] Illusion 1.0", download_url: "assets/skins/Illusion1-0.kjofol", @@ -165,7 +179,10 @@ async function initializeSkinListMenu() { filename: "[K-Jofol] K-Nine 05r", download_url: "assets/skins/K-Nine05r.kjofol", }, - { filename: "[K-Jofol] Limus 2.0", download_url: "assets/skins/Limus2-0.zip" }, + { + filename: "[K-Jofol] Limus 2.0", + download_url: "assets/skins/Limus2-0.zip", + }, // SONIQUE { filename: "[Sonique] Default", download_url: "assets/skins/sonique.sgf" }, @@ -177,17 +194,26 @@ async function initializeSkinListMenu() { filename: "[Sonique] Panthom (SkinBuilder)", download_url: "assets/skins/phantom.sgf", }, - { filename: "[Sonique] ChainZ and", download_url: "assets/skins/ChainZ-and.sgf" }, + { + filename: "[Sonique] ChainZ and", + download_url: "assets/skins/ChainZ-and.sgf", + }, // COWON JET-AUDIO { filename: "[JetAudio] Small Bar", download_url: "assets/skins/DefaultBar_s.jsk", }, - { filename: "[Cowon JetAudio] Gold", download_url: "assets/skins/Gold.uib" }, + { + filename: "[Cowon JetAudio] Gold", + download_url: "assets/skins/Gold.uib", + }, // AIMP - { filename: "[AIMP] Flo-4K", download_url: "assets/skins/AIMP-Flo-4K.acs5" }, + { + filename: "[AIMP] Flo-4K", + download_url: "assets/skins/AIMP-Flo-4K.acs5", + }, ]; const skins = [...internalSkins, ...bankskin1]; diff --git a/packages/webamp-modern/src/maki/interpreter.ts b/packages/webamp-modern/src/maki/interpreter.ts index 7f36ce7afe..9005d2db59 100644 --- a/packages/webamp-modern/src/maki/interpreter.ts +++ b/packages/webamp-modern/src/maki/interpreter.ts @@ -49,7 +49,13 @@ export async function interpret( uiRoot ); interpreter.stack = stack; - return await interpreter.interpret(start); + try { + return await interpreter.interpret(start); + // return interpreter.interpret(start); + } catch (error) { + // console.warn('error while interpret', program.file, error) + console.warn(`Stopped executing ${program.maki_id}.\n`, error); + } } function validateVariable(v: Variable) { diff --git a/packages/webamp-modern/src/skin/Bitmap.ts b/packages/webamp-modern/src/skin/Bitmap.ts index 8813ae2133..f5c9564ee0 100644 --- a/packages/webamp-modern/src/skin/Bitmap.ts +++ b/packages/webamp-modern/src/skin/Bitmap.ts @@ -191,7 +191,7 @@ export default class Bitmap { const groupId = this.getGammaGroup(); const gammaGroup = uiRoot._getGammaGroup(groupId); - if (gammaGroup._value == "0,0,0") { + if (gammaGroup._value == "0,0,0" && gammaGroup._gray == 0) { // triple zero meaning no gamma should be applied. // return bitmap.getCanvas().toDataURL(); const url = await this.toDataURL(uiRoot); diff --git a/packages/webamp-modern/src/skin/SkinEngine_WAL.ts b/packages/webamp-modern/src/skin/SkinEngine_WAL.ts index e0e14e500f..ce97f9ddc4 100644 --- a/packages/webamp-modern/src/skin/SkinEngine_WAL.ts +++ b/packages/webamp-modern/src/skin/SkinEngine_WAL.ts @@ -101,7 +101,7 @@ export default class SkinEngineWAL extends SkinEngine { async prepareArial() { const node: XmlElement = new XmlElement("truetypefont", { id: "Arial", - family: "Arial", + family: "Arial, 'Liberation Sans', 'DejaVu Sans'", }); await this.trueTypeFont(node, null); } diff --git a/packages/webamp-modern/src/skin/VM.ts b/packages/webamp-modern/src/skin/VM.ts index 89646a295b..9c7c272b4d 100644 --- a/packages/webamp-modern/src/skin/VM.ts +++ b/packages/webamp-modern/src/skin/VM.ts @@ -47,7 +47,11 @@ export default class Vm { // This could easily become performance sensitive. We could make this more // performant by normalizing some of these things when scripts are added. - async dispatch(object: BaseObject, event: string, args: Variable[] = []): number { + async dispatch( + object: BaseObject, + event: string, + args: Variable[] = [] + ): number { const reversedArgs = [...args].reverse(); let executed = 0; for (const script of this._scripts) { @@ -83,7 +87,12 @@ export default class Vm { } if (match) { - await this.interpret(script, binding.commandOffset, event, reversedArgs); + await this.interpret( + script, + binding.commandOffset, + event, + reversedArgs + ); // return 1; executed++; } diff --git a/packages/webamp-modern/src/skin/makiClasses/GuiObj.ts b/packages/webamp-modern/src/skin/makiClasses/GuiObj.ts index 16b8b998ea..5ed16cf882 100644 --- a/packages/webamp-modern/src/skin/makiClasses/GuiObj.ts +++ b/packages/webamp-modern/src/skin/makiClasses/GuiObj.ts @@ -20,6 +20,30 @@ import { XmlElement } from "@rgrove/parse-xml"; let BRING_LEAST: number = -1; let BRING_MOST_TOP: number = 1; +const globalMouseDown: Function[] = []; +export const installGlobalMouseDown = (f: Function) => { + if (!globalMouseDown.includes(f)) { + globalMouseDown.push(f); + } +}; +export const uninstallGlobalMouseDown = (f: Function) => { + const index = globalMouseDown.indexOf(f); + if (index != -1) { + // delete globalMouseDown[index] + globalMouseDown.splice(index, 1); + } +}; + +export const executeGlobalMouseDown = (e: MouseEvent) => { + for (let i = 0; i < globalMouseDown.length; i++) { + // console.log(typeof globalMouseDown[i], '>>', globalMouseDown[i]); + globalMouseDown[i](e); + } + // for (let mousedown of globalMouseDown) { + // mousedown(e) + // } +}; + // http://wiki.winamp.com/wiki/XML_GUI_Objects#GuiObject_.28Global_params.29 export default class GuiObj extends XmlObj { static GUID = "4ee3e1994becc636bc78cd97b028869c"; diff --git a/packages/webamp-modern/src/skin/makiClasses/Layout.ts b/packages/webamp-modern/src/skin/makiClasses/Layout.ts index 505a58116c..efde7d2284 100644 --- a/packages/webamp-modern/src/skin/makiClasses/Layout.ts +++ b/packages/webamp-modern/src/skin/makiClasses/Layout.ts @@ -4,6 +4,9 @@ import Container from "./Container"; import { LEFT, RIGHT, TOP, BOTTOM, CURSOR, MOVE } from "../Cursor"; import { px, unimplemented } from "../../utils"; import { UIRoot } from "../../UIRoot"; +import PopupMenu from "./PopupMenu"; +import { forEachMenuItem, IMenuItem } from "./MenuItem"; +import { findAction } from "./menuWa5actions"; // > 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 @@ -17,6 +20,7 @@ export default class Layout extends Group { static GUID = "60906d4e482e537e94cc04b072568861"; _resizingDiv: HTMLDivElement = null; _resizing: boolean = false; + _resizing_start: DOMRect = null; _canResize: number = 0; // combination of 4 directions: N/E/W/S _scale: number = 1.0; _opacity: number = 1.0; @@ -25,6 +29,7 @@ export default class Layout extends Group { _movingStartY: number; _moving: boolean = false; _snap = { left: 0, top: 0, right: 0, bottom: 0 }; + _shortcuts: { [key: string]: number } = {}; constructor(uiRoot: UIRoot) { super(uiRoot); @@ -178,21 +183,33 @@ export default class Layout extends Group { h = this._minimumHeight ? Math.max(h, this._minimumHeight) : h; return h; }; - const container = this._parent; - const r = this._div.getBoundingClientRect(); + // const container = this._parent; if (cmd == "constraint") { this._canResize = dx; } else if (cmd == "start") { this.bringtofront(); + const r = this._div.getBoundingClientRect(); + // r.x = container.getleft() + // r.y = container.gettop() + this._resizing_start = r; + // this._resizing_o = r; this._resizing = true; this._resizingDiv = document.createElement("div"); this._resizingDiv.className = "resizing"; - this._resizingDiv.style.cssText = "position:fixed;"; - this._resizingDiv.style.width = px(r.width); - this._resizingDiv.style.height = px(r.height); - this._resizingDiv.style.top = px(container.gettop()); - this._resizingDiv.style.left = px(container.getleft()); - this._div.appendChild(this._resizingDiv); + // this._resizingDiv.style.cssText = "position:fixed;"; + // this._resizingDiv.style.width = px(r.width); + // this._resizingDiv.style.height = px(r.height); + // this._resizingDiv.style.top = px(container.gettop()); + // this._resizingDiv.style.left = px(container.getleft()); + const { left, top, width, height } = r; + this._resizingDiv.style.cssText = ` + width: ${px(width)}; + height: ${px(height)}; + left: ${px(left)}; + top: ${px(top)}; + `; + // this._div.appendChild(this._resizingDiv); + document.body.appendChild(this._resizingDiv); } else if (dx == CURSOR && dy == CURSOR) { this._resizingDiv.style.cursor = cmd; } else if (cmd == "move") { @@ -200,19 +217,34 @@ export default class Layout extends Group { return; } // console.log(`resizing dx:${dx} dy:${dy}`); - if (this._canResize & RIGHT) - this._resizingDiv.style.width = px(clampW(r.width + dx)); - if (this._canResize & BOTTOM) - this._resizingDiv.style.height = px(clampH(r.height + dy)); + let { left, top, width, height, right, bottom } = this._resizing_start; + if (this._canResize & RIGHT) width = clampW(width + dx); + if (this._canResize & BOTTOM) height = clampH(height + dy); if (this._canResize & LEFT) { - this._resizingDiv.style.left = px(container.getleft() + dx); - this._resizingDiv.style.width = px(clampW(r.width + -dx)); + width = clampW(width + -dx); + let l = left + dx; + if (l + width <= right) { + left = l; + } else { + left = right - this._minimumWidth; + } } if (this._canResize & TOP) { - this._resizingDiv.style.top = px(container.gettop() + dy); - this._resizingDiv.style.height = px(clampH(r.height + -dy)); + height = clampH(height + -dy); + let t = top + dy; + if (t + height <= bottom) { + top = t; + } else { + top = bottom - this._minimumHeight; + } } + this._resizingDiv.style.cssText = ` + width: ${px(width)}; + height: ${px(height)}; + left: ${px(left)}; + top: ${px(top)}; + `; } else if (cmd == "final") { if (!this._resizing) { return; @@ -259,4 +291,21 @@ export default class Layout extends Group { this._moving = false; } } + + // MENU SHORTCUT HANDLER HERE ====================== + registerShortcuts(popup: PopupMenu) { + forEachMenuItem(popup, (m: IMenuItem) => { + if (m.shortcut) { + this._shortcuts[m.shortcut] = m.id; + } + }); + // console.log('layout.shortcuts:', this._shortcuts) + } + + executeShorcut(shortcut: string) { + const menuId = this._shortcuts[shortcut]; + const action = findAction(menuId); + // console.log('Layout:', this._name, 'executing shortcut:',shortcut, '@action.id:', menuId, '=:', action ) + const invalidateRequired = action.onExecute(this._uiRoot); + } } diff --git a/packages/webamp-modern/src/skin/makiClasses/Menu.ts b/packages/webamp-modern/src/skin/makiClasses/Menu.ts index 82839dc01a..68a6d1c83b 100644 --- a/packages/webamp-modern/src/skin/makiClasses/Menu.ts +++ b/packages/webamp-modern/src/skin/makiClasses/Menu.ts @@ -5,10 +5,46 @@ import Group from "./Group"; // import Button from "./Button"; import Layer from "./Layer"; import { getWa5Popup } from "./menuWa5"; -import { generatePopupDiv } from "./PopupMenu"; +import PopupMenu from "./PopupMenu"; +import { + ICLoseablePopup, + destroyActivePopup, + generatePopupDiv, + setActivePopup, +} from "./MenuItem"; +import { findAction, updateActions } from "./menuWa5actions"; +let ACTIVE_MENU_GROUP: string = ""; +// let ACTIVE_MENU: Menu = null; + +/*function destroyActivePopup() { + console.log('globalWindowClick') + if (ACTIVE_MENU != null) { + ACTIVE_MENU.doCloseMenu() + } + ACTIVE_MENU_GROUP = '' +} + +let globalClickInstalled = false; +function installGlobalClickListener() { + setTimeout(() => { // using promise to prevent immediately executing of globalWindowClick + installGlobalMouseDown(destroyActivePopup); // call globalWindowClick on any GuiObj + if (!globalClickInstalled) { + document.addEventListener("mousedown", destroyActivePopup); // call globalWindowClick on document + globalClickInstalled = true; + } + }, 500); +} +function uninstallGlobalClickListener() { + if (globalClickInstalled) { + document.removeEventListener("mousedown", destroyActivePopup); + globalClickInstalled = false; + } + + uninstallGlobalMouseDown(destroyActivePopup) +}*/ // http://wiki.winamp.com/wiki/XML_GUI_Objects#? -export default class Menu extends Group { +export default class Menu extends Group implements ICLoseablePopup { static GUID = "73c00594401b961f24671b9b6541ac27"; //static GUID "73C00594-961F-401B-9B1B-672427AC4165"; _normalId: string; @@ -22,7 +58,8 @@ export default class Menu extends Group { _elHover: GuiObj; _elDown: GuiObj; _elImage: Layer; - _popup: HTMLElement; + _popup: PopupMenu; + _popupDiv: HTMLElement; setXmlAttr(_key: string, value: string): boolean { const key = _key.toLowerCase(); @@ -99,25 +136,67 @@ export default class Menu extends Group { } } } + + doClosePopup() { + this._showButton(this._elNormal); + this._div.classList.remove("open"); + // ACTIVE_MENU = null; + // document.removeEventListener("mousedown", globalWindowClick); + // uninstallGlobalMouseDown(globalWindowClick) + // uninstallGlobalClickListener() + // ACTIVE_MENU_GROUP = '' + } + onLeftButtonDown(x: number, y: number) { - super.onLeftButtonDown(x, y); - this._showButton(this._elDown); + // super.onLeftButtonDown(x, y); + // this._showButton(this._elDown); + //? toggle dropdown visibility + if (ACTIVE_MENU_GROUP != this._menuGroupId) { + ACTIVE_MENU_GROUP = this._menuGroupId; + destroyActivePopup(); + // setTimeout(() => { + // installGlobalMouseDown(globalWindowClick); + // }, 500); + // installGlobalClickListener() + setActivePopup(this); + } else { + ACTIVE_MENU_GROUP = null; + // if (ACTIVE_MENU != null) { + // ACTIVE_MENU.doCloseMenu() + // } + destroyActivePopup(); + } + this.onEnterArea(); } onEnterArea() { - super.onEnterArea(); - this._showButton(this._elHover); - this._div.classList.add("open"); + // super.onEnterArea(); + if (ACTIVE_MENU_GROUP == this._menuGroupId) { + // if (ACTIVE_MENU != null) { + // ACTIVE_MENU.doCloseMenu() + // } + destroyActivePopup(); + this._showButton(this._elDown); + this._div.classList.add("open"); + + // ACTIVE_MENU = this; + setActivePopup(this); + } else { + this._showButton(this._elHover); + } } onLeaveArea() { - super.onLeaveArea(); - this._showButton(this._elNormal); - this._div.classList.remove("open"); + // super.onLeaveArea(); + if (ACTIVE_MENU_GROUP != this._menuGroupId) { + this._showButton(this._elNormal); + // this._div.classList.remove("open"); + } } - init() { - super.init(); + setup() { + super.setup(); // this.resolveButtonsAction(); // this._uiRoot.vm.dispatch(this, "onstartup", []); + this.getparentlayout().registerShortcuts(this._popup); } resolveButtonsAction() { //console.log('found img') @@ -157,20 +236,45 @@ export default class Menu extends Group { // this._div.classList.remove("vertical"); // } - - if(this._menuId.startsWith('WA5:')){ - const [,popupId] = this._menuId.split(':') - const popupMenu = getWa5Popup(popupId) + if (this._menuId.startsWith("WA5:")) { + const [, popupId] = this._menuId.split(":"); + this._popup = getWa5Popup(popupId, this._uiRoot); + + this.invalidatePopup(); // function menuClick(id:number){ // console.log('menu clicked:', id) // } - this._popup = generatePopupDiv(popupMenu, (id:number) => console.log('menu clicked:', id)) - } else { - this._popup = document.createElement("div"); - this._popup.classList.add("fake-popup"); } - this._popup.classList.add("popup"); - // this._appendChildrenToDiv(this._popup); - this._div.appendChild(this._popup); + } + + /** + * update the checkmark, enabled/disabled, etc + */ + invalidatePopup() { + const self = this; + if (this._popup) { + // destroy old DOM + if (this._popupDiv) { + this._popupDiv.remove(); + } + + updateActions(this._popup, this._uiRoot); // let winamp5 menus reflect the real config/condition + + const menuItemClick = (id: number) => { + console.log("menu clicked:", id); + const action = findAction(id); + const invalidateRequired = action.onExecute(self._uiRoot); + if (invalidateRequired) self.invalidatePopup(); + }; + + this._popupDiv = generatePopupDiv(this._popup, menuItemClick); + // } else { + // this._popupDiv = document.createElement("div"); + // this._popupDiv.classList.add("fake-popup"); + // } + this._popupDiv.classList.add("popup"); + // this._appendChildrenToDiv(this._popup); + this._div.appendChild(this._popupDiv); + } } } diff --git a/packages/webamp-modern/src/skin/makiClasses/MenuItem.ts b/packages/webamp-modern/src/skin/makiClasses/MenuItem.ts new file mode 100644 index 0000000000..cd7bcf2e83 --- /dev/null +++ b/packages/webamp-modern/src/skin/makiClasses/MenuItem.ts @@ -0,0 +1,197 @@ +import { installGlobalMouseDown, uninstallGlobalMouseDown } from "./GuiObj"; + +export interface IPopupMenu { + children: MenuItem[]; +} + +export type IMenuItem = { + type: "menuitem"; + caption: string; + id: number; + checked: boolean; + disabled?: boolean; + shortcut?: string; // "Ctrl+Alt+Shift+A" + keychar?: string; // 'p' of "&Play" + invisible?: boolean; // special case to register shortcut only + data?: { [key: string]: any }; // used by skin's popup item +}; + +type IMenuSeparator = { + type: "separator"; +}; + +type IMenuPopup = { + type: "popup"; + caption: string; + popup: IPopupMenu; + checked?: boolean; + disabled?: boolean; + children?: MenuItem[]; +}; + +export type MenuItem = IMenuItem | IMenuSeparator | IMenuPopup; + +export interface ICLoseablePopup { + doClosePopup: Function; +} + +// ################## Popup Utils ##########################33 + +let ACTIVE_POPUP: ICLoseablePopup = null; + +export function destroyActivePopup() { + console.log("globalWindowClick"); + if (ACTIVE_POPUP != null) { + ACTIVE_POPUP.doClosePopup(); + } + // ACTIVE_MENU_GROUP = '' + uninstallGlobalClickListener(); +} + +export function setActivePopup(popup: ICLoseablePopup) { + ACTIVE_POPUP = popup; + if (popup) { + installGlobalClickListener(); + } else { + uninstallGlobalClickListener(); + } +} + +/** + * if the active == popup => set null + * @param popup + */ +export function deactivePopup(popup: ICLoseablePopup) { + if (popup == ACTIVE_POPUP) { + setActivePopup(null); + } +} + +let globalClickInstalled = false; +function installGlobalClickListener() { + setTimeout(() => { + // using promise to prevent immediately executing of globalWindowClick + if (!globalClickInstalled) { + installGlobalMouseDown(destroyActivePopup); // call globalWindowClick on any GuiObj + document.addEventListener("mousedown", destroyActivePopup); // call globalWindowClick on document + globalClickInstalled = true; + } + }, 500); +} + +function uninstallGlobalClickListener() { + if (globalClickInstalled) { + document.removeEventListener("mousedown", destroyActivePopup); + globalClickInstalled = false; + uninstallGlobalMouseDown(destroyActivePopup); + } +} + +// ################## MenuItem Utils ##########################33 + +export function forEachMenuItem(popup: IPopupMenu, callback: Function) { + for (const menu of popup.children) { + if (menu.type == "menuitem") { + callback(menu); + } else if (menu.type == "popup") { + forEachMenuItem(menu.popup, callback); + } + } +} + +type ExtractedCaptions = { + caption: string; + shortcut?: string; // "Ctrl+Alt+Shift+A" + keychar?: string; // 'p' of "&Play" +}; + +export function extractCaption(text: string): ExtractedCaptions { + // const result:ExtractedCaptions = {caption:text, shortcut:null, keychar:''} + const [caption, shortcut] = text.split("\t"); + const keychar = caption.includes("&") + ? caption[caption.indexOf("&") + 1].toLowerCase() + : ""; + return { caption, shortcut, keychar }; +} + +export function generatePopupDiv( + popup: IPopupMenu, + callback: Function +): HTMLElement { + const root = document.createElement("ul"); + root.className = "popup-menu-container"; + // root.style.zIndex = "1000"; + // console.log('generating popup:', popup) + for (const menu of popup.children) { + // const menuitem = document.createElement("li"); + let item: HTMLElement; + // root.appendChild(item); + switch (menu.type) { + case "menuitem": + if (menu.invisible === true) { + continue; + } + item = generatePopupItem(menu); + item.addEventListener("mousedown", (e) => callback(menu.id)); + // item.onclick = (e) => callback(menu.id); + break; + case "popup": + item = generatePopupItem(menu); + const subMenu = generatePopupDiv(menu.popup, callback); + item.appendChild(subMenu); + break; + case "separator": + item = document.createElement("hr"); + break; + } + root.appendChild(item); + } + return root; +} + +//? one row of popup +function generatePopupItem(menu: IMenuItem | IMenuPopup): HTMLElement { + const item = document.createElement("li"); + + //? checkmark + const checkMark = document.createElement("span"); + checkMark.classList.add("checkmark"); + checkMark.textContent = menu.checked ? "✓" : " "; + item.appendChild(checkMark); + + //? display text + // @ts-ignore + // const [caption, keystroke] = menu.caption.split('\t') + const label = generateCaption(menu.caption); + label.classList.add("caption"); + item.appendChild(label); + + //? keystroke + + const shortcut = document.createElement("span"); + shortcut.classList.add("keystroke"); + shortcut.textContent = menu.type == "menuitem" ? menu.shortcut : ""; + item.appendChild(shortcut); + + //? sub-menu sign + const chevron = document.createElement("span"); + chevron.classList.add("chevron"); + chevron.textContent = menu.type == "popup" ? "🞂" : " "; + item.appendChild(chevron); + // item.textContent = `${menu.checked? '✓' : ' '} ${menu.caption}`; + + return item; +} + +function generateCaption(caption: string): HTMLElement { + const regex = /(&(\w))/gm; + const subst = `$2`; + + // The substituted value will be contained in the result variable + caption = caption.replace(regex, subst); + + const span = document.createElement("span"); + span.classList.add("caption"); + span.innerHTML = caption; + return span; +} diff --git a/packages/webamp-modern/src/skin/makiClasses/Movable.ts b/packages/webamp-modern/src/skin/makiClasses/Movable.ts index 530d1215a5..ceb544d1b4 100644 --- a/packages/webamp-modern/src/skin/makiClasses/Movable.ts +++ b/packages/webamp-modern/src/skin/makiClasses/Movable.ts @@ -119,29 +119,31 @@ export default abstract class Movable extends GuiObj { CURSOR, CURSOR ); - const startX = downEvent.clientX; - const startY = downEvent.clientY; + const startX = downEvent.pageX; + const startY = downEvent.pageY; const handleMove = (moveEvent: MouseEvent) => { - const newMouseX = moveEvent.clientX; - const newMouseY = moveEvent.clientY; + const newMouseX = moveEvent.pageX; + const newMouseY = moveEvent.pageY; const deltaY = newMouseY - startY; const deltaX = newMouseX - startX; layout.setResizing("move", deltaX, deltaY); }; + const trottledMove = throttle(handleMove, 5); + const handleMouseUp = (upEvent: MouseEvent) => { upEvent.stopPropagation(); if (upEvent.button != 0) return; // only care LeftButton - document.removeEventListener("mousemove", handleMove); + document.removeEventListener("mousemove", trottledMove); document.removeEventListener("mouseup", handleMouseUp); - const newMouseX = upEvent.clientX; - const newMouseY = upEvent.clientY; + const newMouseX = upEvent.pageX; + const newMouseY = upEvent.pageY; const deltaY = newMouseY - startY; const deltaX = newMouseX - startX; layout.setResizing("final", deltaX, deltaY); }; - document.addEventListener("mousemove", throttle(handleMove, 75)); + document.addEventListener("mousemove", trottledMove); document.addEventListener("mouseup", handleMouseUp); }; @@ -165,29 +167,32 @@ export default abstract class Movable extends GuiObj { if (downEvent.button != 0) return; // only care LeftButton const layout = this.getparentlayout() as Layout; layout.setMoving("start", 0, 0); - const startX = downEvent.clientX; - const startY = downEvent.clientY; + const startX = downEvent.pageX; + const startY = downEvent.pageY; const handleMove = (moveEvent: MouseEvent) => { - const newMouseX = moveEvent.clientX; - const newMouseY = moveEvent.clientY; + const newMouseX = moveEvent.pageX; + const newMouseY = moveEvent.pageY; const deltaY = newMouseY - startY; const deltaX = newMouseX - startX; + // console.log("move", deltaX, deltaY); layout.setMoving("move", deltaX, deltaY); }; + const trottledMove = throttle(handleMove, 5); + const handleMouseUp = (upEvent: MouseEvent) => { if (upEvent.button != 0) return; // only care LeftButton upEvent.stopPropagation(); - document.removeEventListener("mousemove", handleMove); + document.removeEventListener("mousemove", trottledMove); document.removeEventListener("mouseup", handleMouseUp); - const newMouseX = upEvent.clientX; - const newMouseY = upEvent.clientY; + const newMouseX = upEvent.pageX; + const newMouseY = upEvent.pageY; const deltaY = newMouseY - startY; const deltaX = newMouseX - startX; layout.setMoving("final", deltaX, deltaY); }; - document.addEventListener("mousemove", throttle(handleMove, 10)); + document.addEventListener("mousemove", trottledMove); document.addEventListener("mouseup", handleMouseUp); }; diff --git a/packages/webamp-modern/src/skin/makiClasses/PopupMenu.ts b/packages/webamp-modern/src/skin/makiClasses/PopupMenu.ts index dd7e53239d..e8e0582d18 100644 --- a/packages/webamp-modern/src/skin/makiClasses/PopupMenu.ts +++ b/packages/webamp-modern/src/skin/makiClasses/PopupMenu.ts @@ -1,23 +1,36 @@ import BaseObject from "./BaseObject"; -import { assume } from "../../utils"; +import { assume, px } from "../../utils"; +import { + MenuItem, + IPopupMenu, + generatePopupDiv, + extractCaption, + ICLoseablePopup, + destroyActivePopup, + setActivePopup, + deactivePopup, + IMenuItem, +} from "./MenuItem"; +import { Skin, UIRoot } from "../../UIRoot"; +import { registerAction } from "./menuWa5actions"; // import { sleep } from 'deasync'; // import { deasync } from '@kaciras/deasync'; // import sp from 'synchronized-promise'; // taken from sp test -const asyncFunctionBuilder = - (success) => - (value, timeouts = 1000) => { - return new Promise((resolve, reject) => { - setTimeout(function () { - if (success) { - resolve(value); - } else { - reject(new TypeError(value)); - } - }, timeouts); - }); - }; +// const asyncFunctionBuilder = +// (success) => +// (value, timeouts = 1000) => { +// return new Promise((resolve, reject) => { +// setTimeout(function () { +// if (success) { +// resolve(value); +// } else { +// reject(new TypeError(value)); +// } +// }, timeouts); +// }); +// }; // const async_sleep = (timeout) => { // // setTimeout(() => done(null, "wake up!"), timeout); // const done = () => {} @@ -26,148 +39,103 @@ const asyncFunctionBuilder = // const sleep = sp(async_sleep) // const sleep = sp(asyncFunctionBuilder(true)) -export type MenuItem = - | { - type: "menuitem"; - caption: string; - id: number; - checked: boolean; - disabled?: boolean; - } - | { type: "separator" } - | { - type: "popup"; - caption: string; - popup: PopupMenu; - disabled?: boolean; - children?: MenuItem[]; - }; +function waitPopup(popup: PopupMenu, x = 0, y = 0): Promise { + // const closePopup = () => div.remove(); - function waitPopup(popup: PopupMenu): Promise { - // const closePopup = () => div.remove(); - - // https://stackoverflow.com/questions/54916739/wait-for-click-event-inside-a-for-loop-similar-to-prompt - return new Promise(acc => { + // https://stackoverflow.com/questions/54916739/wait-for-click-event-inside-a-for-loop-similar-to-prompt + return new Promise((acc) => { // let result: number = -1; const itemClick = (id: number) => { - closePopup() + closePopup(); // result = id; acc(id); }; const div = generatePopupDiv(popup, itemClick); + if (x || y) { + div.style.left = px(x); + div.style.top = px(y); + } document.getElementById("web-amp").appendChild(div); - const closePopup = () => div.remove() + const closePopup = () => { + div.remove(); + popup._successPromise = null; + }; + const outsideClick = (ret: number) => { + closePopup(); + acc(ret); + }; + popup._successPromise = outsideClick; function handleClick() { - document.removeEventListener('click', handleClick); - closePopup() + document.removeEventListener("click", handleClick); + closePopup(); acc(-1); } - document.addEventListener('click', handleClick); + document.addEventListener("click", handleClick); }); // return 1; } -export function generatePopupDiv(popup: PopupMenu, callback: Function): HTMLElement { - const root = document.createElement("ul"); - root.className = 'popup-menu-container' - // root.style.zIndex = "1000"; - // console.log('generating popup:', popup) - for (const menu of popup._items) { - // const menuitem = document.createElement("li"); - let item: HTMLElement; - // root.appendChild(item); - switch (menu.type) { - case "menuitem": - item = generatePopupItem(menu); - item.onclick = (e) => callback(menu.id); - break; - case "popup": - item = generatePopupItem(menu); - const subMenu = generatePopupDiv(menu.popup, callback); - item.appendChild(subMenu) - break; - case "separator": - item = document.createElement("hr"); - break; - } - root.appendChild(item); - } - return root; -} - -//? one row of popup -function generatePopupItem(menu: MenuItem): HTMLElement { - const item = document.createElement("li"); - - //? checkmark - const checkMark = document.createElement("span"); - checkMark.classList.add('checkmark') - checkMark.textContent = menu.checked? '✓' : ' '; - item.appendChild(checkMark) - - //? display text - const [caption, keystroke] = menu.caption.split('\t') - const label = generateCaption(caption); - label.classList.add('caption') - item.appendChild(label) - - //? keystroke - const shortcut = document.createElement("span"); - shortcut.classList.add('keystroke') - shortcut.textContent = keystroke; - item.appendChild(shortcut) - - //? sub-menu sign - const chevron = document.createElement("span"); - chevron.classList.add('chevron') - chevron.textContent = menu.type=='popup'? '⮀' : ' '; - item.appendChild(chevron) - // item.textContent = `${menu.checked? '✓' : ' '} ${menu.caption}`; - - return item; -} - -function generateCaption(caption: string): HTMLElement { - const regex = /(&(\w))/gm; - const subst = `$2`; - - // The substituted value will be contained in the result variable - caption = caption.replace(regex, subst); +export default class PopupMenu + extends BaseObject + implements IPopupMenu, ICLoseablePopup +{ + static GUID = "f4787af44ef7b2bb4be7fb9c8da8bea9"; + children: MenuItem[] = []; + _uiRoot: UIRoot; - const span = document.createElement("span"); - span.classList.add('caption') - span.innerHTML = caption; - return span -} + constructor(uiRoot: UIRoot) { + super(); + this._uiRoot = uiRoot; -export default class PopupMenu extends BaseObject { - static GUID = "f4787af44ef7b2bb4be7fb9c8da8bea9"; - _items: MenuItem[] = []; - addcommand( + // this._div = document.createElement( + // this.getElTag().toLowerCase().replace("_", "") + // ); + } + private _addcommand( cmdText: string, cmd_id: number, - checked: boolean, - disabled: boolean + checked: boolean = false, + disabled: boolean = false, + data: { [key: string]: any } = {} ) { - this._items.push({ + this.children.push({ type: "menuitem", - caption: cmdText, + // caption: cmdText, + ...extractCaption(cmdText), id: cmd_id, checked, disabled, + data, }); } + addcommand( + cmdText: string, + cmd_id: number, + checked: boolean, + disabled: boolean + ) { + if (cmd_id == 32767) { + this._loadSkins(); + return; + } + this._addcommand(cmdText, cmd_id, checked, disabled); + } addseparator() { - this._items.push({ type: "separator" }); + this.children.push({ type: "separator" }); } addsubmenu(popup: PopupMenu, submenutext: string) { - this._items.push({ type: "popup", popup: popup, caption: submenutext }); + // this.children.push({ type: "popup", popup: popup, caption: submenutext }); + this.children.push({ + type: "popup", + popup: popup, + ...extractCaption(submenutext), + }); // // TODO: // this.addcommand(submenutext, 0, false, false) } checkcommand(cmd_id: number, check: boolean) { - const item = this._items.find((item) => { + const item = this.children.find((item) => { return item.type === "menuitem" && item.id === cmd_id; }); assume(item != null, `Could not find item with id "${cmd_id}"`); @@ -177,25 +145,72 @@ export default class PopupMenu extends BaseObject { item.checked = check; } disablecommand(cmd_id: number, disable: boolean) { - for (const item of this._items) { + for (const item of this.children) { if (item.type == "menuitem" && item.id == cmd_id) { item.disabled = disable; break; } } } + async popatmouse(): Promise { - console.log('popAtMouse.start...:') - const result = await waitPopup(this) - console.log('popAtMouse.return:', result) + console.log("popAtMouse.start...:"); + const mousePos = this._uiRoot._mousePos; + // const result = await waitPopup(this, mousePos.x, mousePos.y) + const result = await this.popatxy(mousePos.x, mousePos.y); + console.log("popAtMouse.return:", result); return result; } - async popatxy(x:number, y:number):Promise{ - return await waitPopup(this) + async popatxy(x: number, y: number): Promise { + destroyActivePopup(); + setActivePopup(this); + const ret = await waitPopup(this, x, y); + deactivePopup(this); + // setActivePopup(this) + // this._showButton(this._elDown); + // this._div.classList.add("open"); + + // ACTIVE_MENU = this; + return ret; + } + + /** + * called by such Menu to close this pupup in favour of + * that Menu want to show their own popup (user click that Menu) + */ + doClosePopup() { + if (this._successPromise) { + this._successPromise(-1); + } + } + _successPromise: Function = null; + + _loadSkins() { + let action_id = 32767; + this._uiRoot._skins.forEach((skin) => { + const name = typeof skin === "string" ? skin : skin.name; + const url = typeof skin === "string" ? skin : skin.url; + const skin_info: Skin = { name, url }; + action_id++; + + registerAction(action_id, { + //? Skin, checked or not + onUpdate: (menu: IMenuItem, uiRoot: UIRoot) => { + menu.checked = + uiRoot.getSkinName() == menu.caption || + uiRoot.getSkinUrl() == menu.data.url; + }, + onExecute: (uiRoot: UIRoot) => { + uiRoot.switchSkin(skin_info); + return true; + }, + }); + this._addcommand(name, action_id, false, false, skin_info); + }); } // popatmouse(): number { - // const message = this._items.map((item) => { + // const message = this.children.map((item) => { // switch (item.type) { // case "separator": // return "------"; @@ -206,7 +221,7 @@ export default class PopupMenu extends BaseObject { // message.unshift("Pick the number matching your choice:\n"); // let choice: number | null = null; // while ( - // !this._items.some((item) => item.type === "item" && item.id === choice) + // !this.children.some((item) => item.type === "item" && item.id === choice) // ) { // choice = Number(window.prompt(message.join("\n"))); // if (choice == 0) break; @@ -219,6 +234,15 @@ export default class PopupMenu extends BaseObject { // return this.popatmouse(); // } getnumcommands() { - return this._items.length; + return this.children.length; + } + + hideMenu(cmd_id: number) { + for (const item of this.children) { + if (item.type == "menuitem" && item.id == cmd_id) { + item.invisible = true; + break; + } + } } } diff --git a/packages/webamp-modern/src/skin/makiClasses/SystemObject.ts b/packages/webamp-modern/src/skin/makiClasses/SystemObject.ts index 10681fdbc4..5f04bb6c20 100644 --- a/packages/webamp-modern/src/skin/makiClasses/SystemObject.ts +++ b/packages/webamp-modern/src/skin/makiClasses/SystemObject.ts @@ -622,7 +622,7 @@ export default class SystemObject extends BaseObject { */ newdynamiccontainer(container_id: string): Container { //TODO - return unimplemented(null) + return unimplemented(null); } /** diff --git a/packages/webamp-modern/src/skin/makiClasses/Text.ts b/packages/webamp-modern/src/skin/makiClasses/Text.ts index f3f4946fb1..24901b9189 100644 --- a/packages/webamp-modern/src/skin/makiClasses/Text.ts +++ b/packages/webamp-modern/src/skin/makiClasses/Text.ts @@ -66,6 +66,9 @@ export default class Text extends GuiObj { case "default": // (str) A static string to be displayed. // console.log('THETEXT', value) + // if (value.startsWith(':')) { + // value = this._interpolateText(value) + // } this._text = value; this._renderText(); break; @@ -348,6 +351,24 @@ offsety - (int) Extra pixels to be added to or subtracted from the calculated x ]); } + _interpolateText(value: string): string { + switch (value.toLowerCase()) { + case ":componentname": + const layout = this.getparentlayout(); + if (layout) { + //debugger + try { + // sometime error with wasabi + return layout.getcontainer()._name || value; + } catch { + return value; + } + } + break; + } + return value; + } + gettext(): string { if (this._alternateText) { // alternate text is used in Winamp3 to show a hint of a Play button while mouse down. @@ -542,7 +563,7 @@ offsety - (int) Extra pixels to be added to or subtracted from the calculated x _getBitmapFontTextWidth(font: BitmapFont): number { const charWidth = font._charWidth; - return this.gettext().length * charWidth + this._paddingX * 2; + return this.gettext().length * charWidth; } _getTrueTypeTextWidth(font: TrueTypeFont): number { @@ -554,14 +575,26 @@ offsety - (int) Extra pixels to be added to or subtracted from the calculated x * * @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393 */ - const self = this; + // const self = this; + let txt = this.gettext(); + if (this._forceuppercase) { + txt = txt.toUpperCase(); + } else if (this._forcelowercase) { + txt = txt.toLowerCase(); + } + const fontFamily = + (font && font.getFontFamily()) || + '"Liberation Sans", "DejaVu Sans", Arial'; + const canvas = document.createElement("canvas"); const context = canvas.getContext("2d"); - context.font = `${this._fontSize || 11}px ${ - (font && font.getFontFamily()) || "Arial" - }`; + context.font = `${this._bold ? "700" : ""} ${ + this._fontSize || 11 + }px ${fontFamily}`; + const metrics = context.measureText(this.gettext()); - return metrics.width + self._paddingX * 2; + + return Math.ceil(metrics.width /*+ self._paddingX * 2*/); } draw() { diff --git a/packages/webamp-modern/src/skin/makiClasses/Vis.ts b/packages/webamp-modern/src/skin/makiClasses/Vis.ts index 4f2ac1aad2..4a40571b26 100644 --- a/packages/webamp-modern/src/skin/makiClasses/Vis.ts +++ b/packages/webamp-modern/src/skin/makiClasses/Vis.ts @@ -21,23 +21,23 @@ export class VisPaintHandler { * Attemp to build cached bitmaps for later use while render a frame. * Purpose: fast rendering in animation loop */ - prepare() { } + prepare() {} /** * Called once per frame rendiring */ - paintFrame() { } + paintFrame() {} /** * Attemp to cleanup cached bitmaps */ - dispose() { } + dispose() {} /** * called if it is an AVS. * @param action vis_prev | vis_next | vis_f5 (fullscreen) | */ - doAction(action: string, param: string) { } + doAction(action: string, param: string) {} } // type VisPaintHandlerClass = {new(vis: Vis): VisPaintHandler;}; @@ -721,15 +721,19 @@ class WavePaintHandler extends VisPaintHandler { } if (using16temporaryCanvas) { - const canvas = this._vis._canvas - const visCtx = canvas.getContext('2d'); + const canvas = this._vis._canvas; + const visCtx = canvas.getContext("2d"); visCtx.clearRect(0, 0, canvas.width, canvas.height); visCtx.drawImage( this._16h, - 0, 0, // sx,sy - 72, 16, // sw,sh - 0, 0, //dx,dy - canvas.width, canvas.height //dw,dh + 0, + 0, // sx,sy + 72, + 16, // sw,sh + 0, + 0, //dx,dy + canvas.width, + canvas.height //dw,dh ); } } @@ -804,10 +808,14 @@ class WavePaintHandler extends VisPaintHandler { for (y = top; y <= bottom; y++) { this._ctx.drawImage( this._bar, - 0, colorIndex, // sx,sy - 1, 1, // sw,sh - x, y, //dx,dy - 1, 1 //dw,dh + 0, + colorIndex, // sx,sy + 1, + 1, // sw,sh + x, + y, //dx,dy + 1, + 1 //dw,dh ); } } @@ -815,10 +823,14 @@ class WavePaintHandler extends VisPaintHandler { paintWavDot(x: number, y: number, colorIndex: number) { this._ctx.drawImage( this._bar, - 0, colorIndex, // sx,sy - 1, 1, // sw,sh - x, y, //dx,dy - 1, 1 //dw,dh + 0, + colorIndex, // sx,sy + 1, + 1, // sw,sh + x, + y, //dx,dy + 1, + 1 //dw,dh ); } @@ -835,10 +847,14 @@ class WavePaintHandler extends VisPaintHandler { for (y = top; y <= bottom; y++) { this._ctx.drawImage( this._bar, - 0, colorIndex, // sx,sy - 1, 1, // sw,sh - x, y, //dx,dy - 1, 1 //dw,dh + 0, + colorIndex, // sx,sy + 1, + 1, // sw,sh + x, + y, //dx,dy + 1, + 1 //dw,dh ); } } diff --git a/packages/webamp-modern/src/skin/makiClasses/menuWa5.ts b/packages/webamp-modern/src/skin/makiClasses/menuWa5.ts index c691681e0a..b3f206e6c8 100644 --- a/packages/webamp-modern/src/skin/makiClasses/menuWa5.ts +++ b/packages/webamp-modern/src/skin/makiClasses/menuWa5.ts @@ -1,217 +1,243 @@ -import PopupMenu, { MenuItem } from "./PopupMenu"; - -export function getWa5Popup(popupId: string): PopupMenu { - if(['PE_Help', 'ML_Help'].includes(popupId)) popupId = 'Help'; - const res = wa5commonRes.includes(popupId) ? wa5commonRes : wa5miscRes.includes(popupId) ? popupId : ''; - // const popupJson =getPopupJson(popupId, res); - // console.log('FOUND', popupId, popupJson) - const popup =getPopupMenu(popupId, res); - console.log('FOUND', popupId, popup) - return popup; +import { UIRoot } from "../../UIRoot"; +import PopupMenu from "./PopupMenu"; +import { MenuItem } from "./MenuItem"; + +export function getWa5Popup(popupId: string, uiRoot: UIRoot): PopupMenu { + if (["PE_Help", "ML_Help"].includes(popupId)) popupId = "Help"; + else if (popupId.toLowerCase() == "presets") popupId = "EQpresets"; + const id = `POPUP "${popupId}"`; + const res = wa5commonRes.includes(id) + ? wa5commonRes + : wa5miscRes.includes(id) + ? wa5miscRes + : wa5controlRes; + // const res = wa5commonRes.includes(popupId) ? wa5commonRes : wa5miscRes.includes(popupId) ? popupId : ''; + const popup = getPopupJson(popupId, res, uiRoot); + // console.log('FOUND', popupId, popupJson) + // const popup =getPopupMenu(popupId, res); + console.log("FOUND", popupId, popup); + return popup; } -function getPopupJson(popupId: string, res:string): MenuItem[] { - const root = []; - let container = root; - let levelStack = [root]; - let popup: PopupMenu = null; - let popupStack: PopupMenu[] = []; - let found = false; - // let currentItem = null - - // Looping setiap baris pada string menu - for (let line of res.split('\n')) { - line = line.trim() - - if(!found && !line.startsWith('POPUP')) // skip until found popup - continue; - - // Mengabaikan baris yang tidak penting - if (!line || line.startsWith('//')) { - continue; +function getPopupJson(popupId: string, res: string, uiRoot: UIRoot): PopupMenu { + let root: PopupMenu; + // let container = root; + // let levelStack = [root]; + let popup: PopupMenu = null; + let popupStack: PopupMenu[] = []; + let found = false; + // let currentItem = null + + // Looping setiap baris pada string menu + for (let line of res.split("\n")) { + line = line.trim(); + + if (!found && !line.startsWith("POPUP")) + // skip until found popup + continue; + + // Mengabaikan baris yang tidak penting + if (!line || line.startsWith("//")) { + continue; + } + + // Mengambil level pada baris saat ini + // const level = line.search(/\S/); + + // Mengecek apakah baris merupakan menu + // const menuMatch = line.match(/\s*(POPUP|MENUITEM)\s+"([^"]+)"(?:\s*,\s*(\d+))?,\s*(\d+),\s*(\d+)/i); + const menuMatch = line.match( + /\s*(POPUP|MENUITEM)\s+(SEPARATOR|"([^"]*)")(?:\s*,\s*(\w+)[\s,]*(.*))?/i + ); + if (menuMatch) { + // console.log('match', menuMatch) + // Mengambil informasi menu + let [, tag, t1, t2, sid, flags] = menuMatch; + const type = + tag == "POPUP" + ? "popup" + : t1 == "SEPARATOR" || (flags || "").indexOf("MFT_SEPARATOR") >= 0 + ? "separator" + : "menuitem"; + const id = parseInt(sid); + + if (!found) { + if (type == "popup" && t2 == popupId) { + found = true; + } else { + continue; // skip } + } - // Mengambil level pada baris saat ini - // const level = line.search(/\S/); - - // Mengecek apakah baris merupakan menu - // const menuMatch = line.match(/\s*(POPUP|MENUITEM)\s+"([^"]+)"(?:\s*,\s*(\d+))?,\s*(\d+),\s*(\d+)/i); - const menuMatch = line.match(/\s*(POPUP|MENUITEM)\s+(SEPARATOR|"([^"]*)")(?:\s*,\s*(\w+)[\s,]*(.*))?/i); - if (menuMatch) { - // console.log('match', menuMatch) - // Mengambil informasi menu - let [, tag, t1, t2, sid, flags] = menuMatch; - const type = tag == 'POPUP' ? 'popup' : (t1 == 'SEPARATOR' || (flags || '').indexOf('MFT_SEPARATOR') >= 0) ? 'separator' : 'menuitem'; - const id = parseInt(sid) - - - if(!found) { - if(type=='popup' && t2 == popupId) { - found = true; - } else { - continue; // skip - } - } - - flags = flags || '' - // Membuat objek menu baru - // @ts-ignore - const menu: MenuItem = { - type, - }; - container.push(menu); // attach to prent - switch (menu.type) { - case 'popup': - const newPopup = new PopupMenu(); - if(popup){ // if it is a sub-popup, attach to parent - popup.addsubmenu(newPopup, t2) - } - popup = newPopup; - popupStack.push(popup); - menu.popup = popup; - menu.caption = t2; - menu.children = []; - container = menu.children; - if (flags.indexOf('GRAYED') >= 0) menu.disabled = true; - levelStack.push(container) - break; - case 'menuitem': - menu.caption = t2; - menu.id = id; - // if(flags.indexOf('GRAYED') >= 0) menu.disabled = true; - menu.disabled = flags.indexOf('GRAYED') >= 0; - popup.addcommand(t2, id, false, menu.disabled) - break; - case 'separator': - popup.addseparator() - break; - } - // const id = type=='popup'? 65535: type == 'separator' ? 0 : parseInt(sid); - - // console.log('m', newMenu, '>>', flags) - // @ts-ignore - // menu.flags = flags; - - // console.log('m', menu) - - } else if (['}', 'END'].includes(line.trim())) { - // Menutup menu saat ini - levelStack.pop(); - container = levelStack[levelStack.length - 1]; - if(found) { - if(container == root) - break; - - popupStack.pop(); - popup = popupStack[popupStack.length - 1]; - } - } + flags = flags || ""; + // Membuat objek menu baru + // @ts-ignore + const menu: MenuItem = { + type, + }; + // container.push(menu); // attach to prent + switch (menu.type) { + case "popup": + const newPopup = new PopupMenu(uiRoot); + if (popup) { + // if it is a sub-popup, attach to parent + popup.addsubmenu(newPopup, t2); + } else { + root = newPopup; + } + popup = newPopup; + popupStack.push(popup); + menu.popup = popup; + menu.caption = t2; + // menu.children = []; + // container = menu.children; + if (flags.indexOf("GRAYED") >= 0) menu.disabled = true; + // levelStack.push(container) + break; + case "menuitem": + menu.caption = t2; + menu.id = id; + // if(flags.indexOf('GRAYED') >= 0) menu.disabled = true; + menu.disabled = flags.indexOf("GRAYED") >= 0; + popup.addcommand(t2, id, false, menu.disabled); + break; + case "separator": + popup.addseparator(); + break; + } + // const id = type=='popup'? 65535: type == 'separator' ? 0 : parseInt(sid); + + // console.log('m', newMenu, '>>', flags) + // @ts-ignore + // menu.flags = flags; + + // console.log('m', menu) + } else if (["}", "END"].includes(line.trim())) { + // Menutup menu saat ini + // levelStack.pop(); + // container = levelStack[levelStack.length - 1]; + if (found) { + if (popup == root) break; + + popupStack.pop(); + popup = popupStack[popupStack.length - 1]; + } } + } - return root; + return root; } -function getPopupMenu(popupId: string, res:string): PopupMenu { - let root: PopupMenu; - // let container = root; - // let levelStack = [root]; - let popup: PopupMenu = null; - let popupStack: PopupMenu[] = []; - let found = false; - // let currentItem = null - - // Looping setiap baris pada string menu - for (let line of res.split('\n')) { - line = line.trim() - - if(!found && !line.startsWith('POPUP')) // skip until found popup - continue; - - // Mengabaikan baris yang tidak penting - if (!line || line.startsWith('//')) { - continue; +function getPopupMenu(popupId: string, res: string, uiRoot: UIRoot): PopupMenu { + let root: PopupMenu; + // let container = root; + // let levelStack = [root]; + let popup: PopupMenu = null; + let popupStack: PopupMenu[] = []; + let found = false; + // let currentItem = null + + // Looping setiap baris pada string menu + for (let line of res.split("\n")) { + line = line.trim(); + + if (!found && !line.startsWith("POPUP")) + // skip until found popup + continue; + + // Mengabaikan baris yang tidak penting + if (!line || line.startsWith("//")) { + continue; + } + + // Mengambil level pada baris saat ini + // const level = line.search(/\S/); + + // Mengecek apakah baris merupakan menu + // const menuMatch = line.match(/\s*(POPUP|MENUITEM)\s+"([^"]+)"(?:\s*,\s*(\d+))?,\s*(\d+),\s*(\d+)/i); + const menuMatch = line.match( + /\s*(POPUP|MENUITEM)\s+(SEPARATOR|"([^"]*)")(?:\s*,\s*(\w+)[\s,]*(.*))?/i + ); + if (menuMatch) { + // console.log('match', menuMatch) + // Mengambil informasi menu + let [, tag, t1, t2, sid, flags] = menuMatch; + const type = + tag == "POPUP" + ? "popup" + : t1 == "SEPARATOR" || (flags || "").indexOf("MFT_SEPARATOR") >= 0 + ? "separator" + : "menuitem"; + const id = parseInt(sid); + + if (!found) { + if (type == "popup" && t2 == popupId) { + found = true; + } else { + continue; // skip } + } - // Mengambil level pada baris saat ini - // const level = line.search(/\S/); - - // Mengecek apakah baris merupakan menu - // const menuMatch = line.match(/\s*(POPUP|MENUITEM)\s+"([^"]+)"(?:\s*,\s*(\d+))?,\s*(\d+),\s*(\d+)/i); - const menuMatch = line.match(/\s*(POPUP|MENUITEM)\s+(SEPARATOR|"([^"]*)")(?:\s*,\s*(\w+)[\s,]*(.*))?/i); - if (menuMatch) { - // console.log('match', menuMatch) - // Mengambil informasi menu - let [, tag, t1, t2, sid, flags] = menuMatch; - const type = tag == 'POPUP' ? 'popup' : (t1 == 'SEPARATOR' || (flags || '').indexOf('MFT_SEPARATOR') >= 0) ? 'separator' : 'menuitem'; - const id = parseInt(sid) - - - if(!found) { - if(type=='popup' && t2 == popupId) { - found = true; - } else { - continue; // skip - } - } - - flags = flags || '' - // Membuat objek menu baru - // @ts-ignore - const menu: MenuItem = { - type, - }; - // container.push(menu); // attach to prent - switch (menu.type) { - case 'popup': - const newPopup = new PopupMenu(); - if(popup){ // if it is a sub-popup, attach to parent - popup.addsubmenu(newPopup, t2) - } else { - root = newPopup - } - popup = newPopup; - popupStack.push(popup); - menu.popup = popup; - menu.caption = t2; - menu.children = []; - // container = menu.children; - if (flags.indexOf('GRAYED') >= 0) menu.disabled = true; - // levelStack.push(container) - break; - case 'menuitem': - menu.caption = t2; - menu.id = id; - // if(flags.indexOf('GRAYED') >= 0) menu.disabled = true; - menu.disabled = flags.indexOf('GRAYED') >= 0; - popup.addcommand(t2, id, false, menu.disabled) - break; - case 'separator': - popup.addseparator() - break; - } - // const id = type=='popup'? 65535: type == 'separator' ? 0 : parseInt(sid); - - // console.log('m', newMenu, '>>', flags) - // @ts-ignore - // menu.flags = flags; - - // console.log('m', menu) - - } else if (['}', 'END'].includes(line.trim())) { - // Menutup menu saat ini - // levelStack.pop(); - // container = levelStack[levelStack.length - 1]; - if(found) { - if(popup == root) - break; - - popupStack.pop(); - popup = popupStack[popupStack.length - 1]; - } - } + flags = flags || ""; + // Membuat objek menu baru + // @ts-ignore + const menu: MenuItem = { + type, + }; + // container.push(menu); // attach to prent + switch (menu.type) { + case "popup": + const newPopup = new PopupMenu(uiRoot); + if (popup) { + // if it is a sub-popup, attach to parent + popup.addsubmenu(newPopup, t2); + } else { + root = newPopup; + } + popup = newPopup; + popupStack.push(popup); + menu.popup = popup; + menu.caption = t2; + // menu.children = []; + // container = menu.children; + if (flags.indexOf("GRAYED") >= 0) menu.disabled = true; + // levelStack.push(container) + break; + case "menuitem": + menu.caption = t2; + menu.id = id; + // if(flags.indexOf('GRAYED') >= 0) menu.disabled = true; + menu.disabled = flags.indexOf("GRAYED") != -1; + popup.addcommand(t2, id, false, menu.disabled); + if (flags.indexOf("HIDDEN") != -1) { + popup.hideMenu(id); + } + break; + case "separator": + popup.addseparator(); + break; + } + // const id = type=='popup'? 65535: type == 'separator' ? 0 : parseInt(sid); + + // console.log('m', newMenu, '>>', flags) + // @ts-ignore + // menu.flags = flags; + + // console.log('m', menu) + } else if (["}", "END"].includes(line.trim())) { + // Menutup menu saat ini + // levelStack.pop(); + // container = levelStack[levelStack.length - 1]; + if (found) { + if (popup == root) break; + + popupStack.pop(); + popup = popupStack[popupStack.length - 1]; + } } + } - return root; + return root; } const wa5commonRes = `256 MENUEX @@ -314,8 +340,9 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US } } MENUITEM "", 0, MFT_SEPARATOR, MFS_ENABLED - MENUITEM "Time &elapsed Ctrl+T toggles", 40037, MFT_STRING, MFS_ENABLED - MENUITEM "Time re&maining Ctrl+T toggles", 40038, MFT_STRING, MFS_ENABLED + MENUITEM "&Time elapsed Alt+T toggles", 40037, MFT_STRING, MFS_ENABLED + MENUITEM "Time re&maining Alt+T toggles", 40038, MFT_STRING, MFS_ENABLED + MENUITEM "Time remaining toggle Alt+T", 40039, MFT_STRING, WEBAMP_HIDDEN MENUITEM "", 0, MFT_SEPARATOR, MFS_ENABLED MENUITEM "&Always On Top Ctrl+A", 40019, MFT_STRING, MFS_ENABLED MENUITEM "&Double Size Ctrl+D", 40165, MFT_STRING, MFS_ENABLED @@ -721,4 +748,73 @@ LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US } } } -`; \ No newline at end of file +`; + +//from gen_ff.dll +const wa5controlRes = `1281 MENU +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +{ + POPUP "ControlMenu" + { + POPUP "&Opacity" + { + MENUITEM "1&00%", 42211 + MENUITEM "&90%", 42210 + MENUITEM "&80%", 42209 + MENUITEM "&70%", 42208 + MENUITEM "&60%", 42207 + MENUITEM "&50%", 42206 + MENUITEM "&40%", 42205 + MENUITEM "&30%", 42204 + MENUITEM "&20%", 42203 + MENUITEM "&10%", 42202 + MENUITEM "&Custom", 42227 + MENUITEM SEPARATOR + MENUITEM "Opaque on &Focus", 42228 + MENUITEM "Opaque on &Hover", 42226 + } + POPUP "&Scaling" + { + MENUITEM "&50%", 42214 + MENUITEM "&75%", 42215 + MENUITEM "&100%", 42216 + MENUITEM "150%", 42222 + MENUITEM "&200%", 42217 + MENUITEM "250%", 42218 + MENUITEM "&300%", 42219 + MENUITEM "&Custom", 42224 + MENUITEM SEPARATOR + MENUITEM "&Locked", 42223 + MENUITEM "&Temporary", 42225 + } + POPUP "Docked Toolbar", 65535, MFT_STRING, MFS_GRAYED, 0 + { + MENUITEM "Auto-&Hide", 42235 + MENUITEM "&Always On Top", 42234 + MENUITEM SEPARATOR + MENUITEM "Top", 42229 + MENUITEM "Left", 42236 + MENUITEM "Right", 42231 + MENUITEM "Bottom", 42232 + MENUITEM "Not docked", 42233 + MENUITEM SEPARATOR + MENUITEM "Dock/Undock Windows by Dragging", 42237 + } + } +}`; + +const ffCustomScaleDialog = `1286 DIALOGEX 0, 0, 165, 70 +STYLE DS_SYSMODAL | DS_SHELLFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU +EXSTYLE WS_EX_TOOLWINDOW +CAPTION "Custom Scale" +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US +FONT 8, "MS Shell Dlg" +{ + CONTROL "Scale : 100%", 1026, BUTTON, BS_GROUPBOX | WS_CHILD | WS_VISIBLE, 7, 7, 151, 37 + CONTROL "Slider1", 1025, "msctls_trackbar32", TBS_HORZ | TBS_BOTH | TBS_NOTICKS | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 13, 17, 139, 11 + CONTROL "10%", -1, STATIC, SS_LEFT | WS_CHILD | WS_VISIBLE | WS_GROUP, 17, 30, 14, 8 + CONTROL "300%", -1, STATIC, SS_LEFT | WS_CHILD | WS_VISIBLE | WS_GROUP, 131, 30, 18, 8 + CONTROL "OK", 1, BUTTON, BS_DEFPUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 54, 50, 50, 13 + CONTROL "Cancel", 2, BUTTON, BS_PUSHBUTTON | WS_CHILD | WS_VISIBLE | WS_TABSTOP, 108, 50, 50, 13 +} +`; diff --git a/packages/webamp-modern/src/skin/makiClasses/menuWa5actions.ts b/packages/webamp-modern/src/skin/makiClasses/menuWa5actions.ts new file mode 100644 index 0000000000..44a49c4fac --- /dev/null +++ b/packages/webamp-modern/src/skin/makiClasses/menuWa5actions.ts @@ -0,0 +1,124 @@ +import { Skin, UIRoot } from "../../UIRoot"; +import PopupMenu from "./PopupMenu"; +import { IMenuItem, IPopupMenu, MenuItem } from "./MenuItem"; + +type MenuActionEvent = (menu: MenuItem, uiRoot: UIRoot) => void; +type MenuActionExecution = (uiRoot: UIRoot) => boolean | void; + +type MenuAction = { + onUpdate?: MenuActionEvent; //? attemp to update disability, checkmark, visiblity, etc + onExecute?: MenuActionExecution; //? function to run when menu is clicked +}; +const dummyAction: MenuAction = { + onUpdate: (menu: MenuItem) => {}, + onExecute: (uiRoot: UIRoot) => false, +}; + +export const actions: { [key: number]: MenuAction } = {}; +export const findAction = (menuId: number): MenuAction => { + const registeredAction = actions[menuId] || {}; + return { ...dummyAction, ...registeredAction }; +}; +export async function updateActions(popup: IPopupMenu, uiRoot: UIRoot) { + return await Promise.all( + popup.children.map(async (menuItem) => { + if (menuItem.type == "menuitem") { + const action = findAction(menuItem.id); + action.onUpdate(menuItem, uiRoot); + } else if (menuItem.type == "popup") { + await updateActions(menuItem.popup, uiRoot); + } + }) + ); +} + +export const registerAction = (menuId: number, action: MenuAction) => { + actions[menuId] = action; +}; + +registerAction(40037, { + //? Time elapsed + onUpdate: (menu: IMenuItem, uiRoot: UIRoot) => { + menu.checked = !uiRoot.audio._timeRemaining; + }, + onExecute: (uiRoot: UIRoot) => { + uiRoot.audio._timeRemaining = false; + return true; + }, +}); + +registerAction(40038, { + //? Time remaining + onUpdate: (menu: IMenuItem, uiRoot: UIRoot) => { + menu.checked = uiRoot.audio._timeRemaining; + }, + onExecute: (uiRoot: UIRoot) => { + uiRoot.audio._timeRemaining = true; + return true; + }, +}); + +registerAction(40039, { + //? Time remaining + onExecute: (uiRoot: UIRoot) => { + uiRoot.audio.toggleRemainingTime(); + return true; + }, +}); + +registerAction(40044, { + //? Previous + onExecute: (uiRoot: UIRoot) => uiRoot.dispatch("prev"), +}); + +registerAction(40045, { + //? Play + onExecute: (uiRoot: UIRoot) => uiRoot.dispatch("play"), +}); + +registerAction(40046, { + //? Pause + onExecute: (uiRoot: UIRoot) => uiRoot.dispatch("pause"), +}); + +registerAction(40047, { + //? Stop + onExecute: (uiRoot: UIRoot) => uiRoot.dispatch("stop"), +}); + +registerAction(40048, { + //? Next + onExecute: (uiRoot: UIRoot) => uiRoot.dispatch("next"), +}); + +registerAction(11111140038, { + //? + onUpdate: (menu: MenuItem) => {}, +}); + +registerAction(40244, { + //? Equalizer Enabled + onUpdate: (menu: IMenuItem, uiRoot: UIRoot) => { + menu.checked = uiRoot.audio._eqEnabled; + }, + onExecute: (uiRoot: UIRoot) => { + uiRoot.eq_toggle(); + return true; + }, +}); + +registerAction(40040, { + //? View/ Playlist Editor + onUpdate: (menu: IMenuItem, uiRoot: UIRoot) => { + menu.checked = uiRoot.getActionState("toggle", "guid:pl"); + }, + onExecute: (uiRoot: UIRoot) => { + uiRoot.dispatch("toggle", "guid:pl"); + return true; + }, +}); + +// registerAction(32767, { //? Skin, checked or not +// onUpdate: (menu: IMenuItem, uiRoot: UIRoot) => {menu.checked = uiRoot.getSkinName() == menu.caption}, +// onExecute: (uiRoot: UIRoot) => { uiRoot.switchSkin(menu.data as Skin); return true }, +// }) diff --git a/packages/webamp-modern/src/xp/menuparser3.ts b/packages/webamp-modern/src/xp/menuparser3.ts index ef79c8c8b1..d68f537697 100644 --- a/packages/webamp-modern/src/xp/menuparser3.ts +++ b/packages/webamp-modern/src/xp/menuparser3.ts @@ -53,11 +53,10 @@ BEGIN BEGIN MENUITEM "&About ...", IDM_ABOUT,MFT_STRING,MFS_ENABLED END -END` +END`; - -// Untuk mengkonversi menu tersebut ke dalam format JSON, -// Anda dapat melakukan parsing manual dengan melakukan split string dan looping. +// Untuk mengkonversi menu tersebut ke dalam format JSON, +// Anda dapat melakukan parsing manual dengan melakukan split string dan looping. // Berikut ini adalah contoh implementasi menggunakan JavaScript: // Membuat fungsi untuk parsing string menu ke dalam format JSON @@ -68,9 +67,9 @@ function parseMenuToJson(menuContent) { // let currentItem = null // Looping setiap baris pada string menu - for (let line of menuContent.split('\n')) { + for (let line of menuContent.split("\n")) { // Mengabaikan baris yang tidak penting - if (!line || line.trim().startsWith('//')) { + if (!line || line.trim().startsWith("//")) { continue; } @@ -79,14 +78,21 @@ function parseMenuToJson(menuContent) { // Mengecek apakah baris merupakan menu // const menuMatch = line.match(/\s*(POPUP|MENUITEM)\s+"([^"]+)"(?:\s*,\s*(\d+))?,\s*(\d+),\s*(\d+)/i); - const menuMatch = line.match(/\s*(POPUP|MENUITEM)\s+(SEPARATOR|"([^"]*)")(?:\s*,\s*(\w+)[\s,]*(.*))?/i); + const menuMatch = line.match( + /\s*(POPUP|MENUITEM)\s+(SEPARATOR|"([^"]*)")(?:\s*,\s*(\w+)[\s,]*(.*))?/i + ); if (menuMatch) { // console.log('match', menuMatch) // Mengambil informasi menu let [, tag, t1, t2, id, flags] = menuMatch; - const type = tag == 'POPUP' ? 'popup' : (t1 == 'SEPARATOR' || (flags || '').indexOf('MFT_SEPARATOR') >= 0) ? 'separator' : 'menuitem'; - - flags = flags || '' + const type = + tag == "POPUP" + ? "popup" + : t1 == "SEPARATOR" || (flags || "").indexOf("MFT_SEPARATOR") >= 0 + ? "separator" + : "menuitem"; + + flags = flags || ""; // Membuat objek menu baru // @ts-ignore const menu: MenuItem = { @@ -104,18 +110,18 @@ function parseMenuToJson(menuContent) { }; container.push(menu); // attach to prent switch (menu.type) { - case 'popup': + case "popup": menu.caption = t2; menu.children = []; container = menu.children; - if (flags.indexOf('GRAYED') >= 0) menu.disabled = true; - levelStack.push(container) + if (flags.indexOf("GRAYED") >= 0) menu.disabled = true; + levelStack.push(container); break; - case 'menuitem': + case "menuitem": menu.caption = t2; menu.id = id; // if(flags.indexOf('GRAYED') >= 0) menu.disabled = true; - flags.indexOf('GRAYED') >= 0 && (menu.disabled = true) + flags.indexOf("GRAYED") >= 0 && (menu.disabled = true); break; default: @@ -127,7 +133,7 @@ function parseMenuToJson(menuContent) { // @ts-ignore // menu.flags = flags; - console.log('m', menu) + console.log("m", menu); // Menambahkan objek menu baru ke dalam parent menu yang sesuai // if (level > levelStack[levelStack.length - 1]) { @@ -142,7 +148,7 @@ function parseMenuToJson(menuContent) { // currentItem.children.push(newMenu); // currentItem = newMenu; // } - } else if (['}', 'END'].includes(line.trim())) { + } else if (["}", "END"].includes(line.trim())) { // Menutup menu saat ini levelStack.pop(); container = levelStack[levelStack.length - 1]; @@ -152,8 +158,8 @@ function parseMenuToJson(menuContent) { return root; } -var m = parseMenuToJson(menuContent) -console.log(m) +var m = parseMenuToJson(menuContent); +console.log(m); -var m = parseMenuToJson(another_sample) -console.log(m) \ No newline at end of file +var m = parseMenuToJson(another_sample); +console.log(m);