merge from latest: x2nie-dev series: 12. (#1255)

This commit is contained in:
Fathony Luthfillah 2024-05-22 19:05:08 +07:00 committed by GitHub
parent 65041ab4d6
commit bb9fb130ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 1397 additions and 531 deletions

View file

@ -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<string> {
// if (this._preferZip) {
// return await this.getFileAsStringZip(filePath);
// } else {
// return await this.getFileAsStringPath(filePath);
// }
// }
// async getFileAsBytes(filePath: string): Promise<ArrayBuffer> {
// if (this._preferZip) {
// return await this.getFileAsBytesZip(filePath);
// } else {
// return await this.getFileAsBytesPath(filePath);
// }
// }
// async getFileAsBlob(filePath: string): Promise<Blob> {
// if (this._preferZip) {
// return await this.getFileAsBlobZip(filePath);
// } else {
// return await this.getFileAsBlobPath(filePath);
// }
// }
// async getFileAsStringZip(filePath: string): Promise<string> {
// if (!filePath) return null;
// const zipObj = getCaseInsensitiveFile(this._zip, filePath);
// if (!zipObj) return null;
// return await zipObj.async("text");
// }
// async getFileAsBytesZip(filePath: string): Promise<ArrayBuffer> {
// if (!filePath) return null;
// const zipObj = getCaseInsensitiveFile(this._zip, filePath);
// if (!zipObj) return null;
// return await zipObj.async("arraybuffer");
// }
// async getFileAsBlobZip(filePath: string): Promise<Blob> {
// if (!filePath) return null;
// const zipObj = getCaseInsensitiveFile(this._zip, filePath);
// if (!zipObj) return null;
// return await zipObj.async("blob");
// }
// async getFileAsStringPath(filePath: string): Promise<string> {
// const response = await fetch(this._skinPath + filePath);
// return await response.text();
// }
// async getFileAsBytesPath(filePath: string): Promise<ArrayBuffer> {
// const response = await fetch(this._skinPath + filePath);
// return await response.arrayBuffer();
// }
// async getFileAsBlobPath(filePath: string): Promise<Blob> {
// 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;
}

View file

@ -13,4 +13,13 @@ button.wasabi {
button.wasabi:active {
border-image-source: var(--bitmap-studio-button-pressed);
}
}
button.center_image::before {
content: '';
position: absolute;
inset: 0;
background-image: var(--background-image);
background-repeat: no-repeat;
background-position: center center;
}

View file

@ -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 {

View file

@ -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;
}
}
/* 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; */
}

View file

@ -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];

View file

@ -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) {

View file

@ -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);

View file

@ -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);
}

View file

@ -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++;
}

View file

@ -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";

View file

@ -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);
}
}

View file

@ -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 {
}
}
}
onLeftButtonDown(x: number, y: number) {
super.onLeftButtonDown(x, y);
this._showButton(this._elDown);
}
onEnterArea() {
super.onEnterArea();
this._showButton(this._elHover);
this._div.classList.add("open");
}
onLeaveArea() {
super.onLeaveArea();
doClosePopup() {
this._showButton(this._elNormal);
this._div.classList.remove("open");
// ACTIVE_MENU = null;
// document.removeEventListener("mousedown", globalWindowClick);
// uninstallGlobalMouseDown(globalWindowClick)
// uninstallGlobalClickListener()
// ACTIVE_MENU_GROUP = ''
}
init() {
super.init();
onLeftButtonDown(x: number, y: number) {
// 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();
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();
if (ACTIVE_MENU_GROUP != this._menuGroupId) {
this._showButton(this._elNormal);
// this._div.classList.remove("open");
}
}
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);
}
}
}

View file

@ -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 = `<u>$2</u>`;
// 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;
}

View file

@ -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);
};

View file

@ -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<number> {
// const closePopup = () => div.remove();
function waitPopup(popup: PopupMenu): Promise<number> {
// 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 = `<u>$2</u>`;
// 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
}
export default class PopupMenu extends BaseObject {
export default class PopupMenu
extends BaseObject
implements IPopupMenu, ICLoseablePopup
{
static GUID = "f4787af44ef7b2bb4be7fb9c8da8bea9";
_items: MenuItem[] = [];
children: MenuItem[] = [];
_uiRoot: UIRoot;
constructor(uiRoot: UIRoot) {
super();
this._uiRoot = uiRoot;
// this._div = document.createElement(
// this.getElTag().toLowerCase().replace("_", "")
// );
}
private _addcommand(
cmdText: string,
cmd_id: number,
checked: boolean = false,
disabled: boolean = false,
data: { [key: string]: any } = {}
) {
this.children.push({
type: "menuitem",
// caption: cmdText,
...extractCaption(cmdText),
id: cmd_id,
checked,
disabled,
data,
});
}
addcommand(
cmdText: string,
cmd_id: number,
checked: boolean,
disabled: boolean
) {
this._items.push({
type: "menuitem",
caption: cmdText,
id: cmd_id,
checked,
disabled,
});
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<number> {
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<number>{
return await waitPopup(this)
async popatxy(x: number, y: number): Promise<number> {
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;
}
}
}
}

View file

@ -622,7 +622,7 @@ export default class SystemObject extends BaseObject {
*/
newdynamiccontainer(container_id: string): Container {
//TODO
return unimplemented(null)
return unimplemented(null);
}
/**

View file

@ -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() {

View file

@ -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
);
}
}

View file

@ -1,217 +1,243 @@
import PopupMenu, { MenuItem } from "./PopupMenu";
import { UIRoot } from "../../UIRoot";
import PopupMenu from "./PopupMenu";
import { MenuItem } from "./MenuItem";
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;
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
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()
// 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;
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
}
}
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];
}
}
// Mengabaikan baris yang tidak penting
if (!line || line.startsWith("//")) {
continue;
}
return root;
// 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(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;
}
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
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()
// 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;
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
}
}
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];
}
}
// Mengabaikan baris yang tidak penting
if (!line || line.startsWith("//")) {
continue;
}
return root;
// 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(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;
}
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
}
}
}
`;
`;
//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
}
`;

View file

@ -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 },
// })

View file

@ -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';
const type =
tag == "POPUP"
? "popup"
: t1 == "SEPARATOR" || (flags || "").indexOf("MFT_SEPARATOR") >= 0
? "separator"
: "menuitem";
flags = flags || ''
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)
var m = parseMenuToJson(another_sample);
console.log(m);