209 lines · 7.6 KB · JavaScript
RawDownload
// SPDX-License-Identifier: Apache-2.0"use strict"; const CLICK_FUNC_CHANNEL = "org.overte.context-menu.click";const ACTIONS_CHANNEL = "org.overte.context-menu.actions";const MAIN_CHANNEL = "org.overte.context-menu"; let registeredActionSets = {};let registeredActionSetParents = {};let registeredActionSetTitles = {};let disabled = false; function ContextMenu_registerActionSet(name, itemData, parent, title, desc) {	registeredActionSets[name] = itemData;	registeredActionSetParents[name] = parent;	registeredActionSetTitles[name] = { title: title, description: desc }; 	Messages.sendLocalMessage(ACTIONS_CHANNEL, JSON.stringify({		func: "register",		name: name,		parent: parent,		actionSet: itemData,		title: title,		description: desc,	}));} function ContextMenu_unregisterActionSet(name) {	delete registeredActionSets[name];	delete registeredActionSetParents[name];	Messages.sendLocalMessage(ACTIONS_CHANNEL, JSON.stringify({func: "unregister", name: name}));} function ContextMenu_messageReceived(channel, msg, senderID, _localOnly) {	if (channel !== MAIN_CHANNEL) { return; }	if (senderID !== MyAvatar.sessionUUID) { return; } 	const data = JSON.parse(msg); 	// re-register anything the context menu might have missed	if (data.func === "startup") {		for (const [name, set] of Object.entries(registeredActionSets)) {			Messages.sendLocalMessage(ACTIONS_CHANNEL, JSON.stringify({				func: "register",				name: name,				parent: registeredActionSetParents[name],				actionSet: set,				title: registeredActionSetTitles[name].title,				description: registeredActionSetTitles[name].description,			}));		}	}} function ContextMenu_editActionSet(name, itemData) {	if (!(name in registeredActionSets)) {		console.error(`ContextMenu_editActionSet: Attempted to edit unregistered action set "${name}"`);		return;	} 	Messages.sendLocalMessage(ACTIONS_CHANNEL, JSON.stringify({		func: "edit",		name: name,		actionSet: itemData,	}));} function ContextMenu_disable() {	if (disabled) { return; } 	Messages.sendLocalMessage(MAIN_CHANNEL, JSON.stringify({ func: "disable" }));	disabled = true;} function ContextMenu_enable() {	if (!disabled) { return; } 	Messages.sendLocalMessage(MAIN_CHANNEL, JSON.stringify({ func: "enable" }));	disabled = false;} Messages.messageReceived.connect(ContextMenu_messageReceived); // This would be helpful for automatically unregistering action sets// registered by a script that's about to close, but see// https://github.com/overte-org/overte/issues/1767Script.scriptEnding.connect(() => {	Messages.messageReceived.disconnect(ContextMenu_messageReceived); 	for (const set of Object.keys(registeredActionSets)) {		Messages.sendLocalMessage(ACTIONS_CHANNEL, JSON.stringify({func: "unregister", name: set}));	} 	if (disabled) {		Messages.sendLocalMessage(MAIN_CHANNEL, JSON.stringify({ func: "enable" }));	}}); /** * @namespace ContextMenu * * See <code>scripts/tutorials/contextMenu</code> for examples. */ // FIXME: How do I express a userdata schema?/** * @namespace userdata.contextMenu * @property {ContextMenu.Action[]|ContextMenu.Action} actions - Either a list of available actions, or one action that will be triggered without opening the context menu. * @property {string} [title] - The title of this action set. The title is above the action buttons and between the page buttons. * @property {string} [description] - The description text of this action set. The description is above the title and page buttons. * @property {boolean} [root=true] - If <code>true</code>, then when the context menu is opened targeting the parent entity it'll open this action set, rather than opening @link{ContextMenu.ROOT_SET}. If <code>false</code>, then this action set will be a child of @link{ContextMenu.OBJECT_SET}. */ /** * A clickable context menu action. * @typedef {Object} Action * @property {string} text * @property {Color} [textColor=[255, 255, 255]] * @property {Color} [backgroundColor=[0, 0, 0]] * @property {number} [backgroundAlpha=0.8] * @property {string} [iconImage] - URL to an image that will be used as an icon, to the left of the action text. * @property {string} [remoteClickFunc] - A string that will be sent with @link{Messages.sendMessage} when clicked. See @link{ContextMenu.CLICK_FUNC_CHANNEL} and @link{ContextMenu.ClickFunc}. * @property {string} [localClickFunc] - A string that will be sent with @link{Messages.sendLocalMessage} when clicked. See @link{ContextMenu.CLICK_FUNC_CHANNEL} and @link{ContextMenu.ClickFunc}. * @property {boolean} [keepMenuOpen=false] - If <code>true</code>, then the context menu will stay open when this action is clicked. Otherwise, the context menu will close when the action is clicked. */ /** * The JSON object passed through @link{ContextMenu.CLICK_FUNC_CHANNEL} when an action is clicked. * @typedef {Object} ClickFunc * @property {string} func - from localClickFunc or remoteClickFunc * @property {Uuid} [targetID] - The targeted avatar or entity UUID */ module.exports = {	/**	 * The message channel that @link{ContextMenu.Action.remoteClickFunc} and @link{ContextMenu.Action.localClickFunc} use.	 * @readonly	 */	CLICK_FUNC_CHANNEL: CLICK_FUNC_CHANNEL, 	/**	 * The top-level set first opened if not overridden by the target.	 * @readonly	 */	ROOT_SET: "_ROOT", 	/**	 * The "My Avatar" submenu in the context menu root.	 * @readonly	 */	SELF_SET: "_SELF", 	/**	 * The "Object" submenu in the context menu root when an object is targeted.	 * @readonly	 */	OBJECT_SET: "_OBJECT", 	/**	 * The "Avatar" submenu in the context menu root when an avatar is targeted.	 * @readonly	 */	AVATAR_SET: "_AVATAR", 	/**	 * Registers a new action set for the context menu.	 * @param {string} name - The name of the action set	 * @param {(Object.<string, ContextMenu.Action>|ContextMenu.Action[])} actions - The action information	 * @param {string} [parent] - The parent of the action set. If no parent is set, the action set will only be accessible through another action's "submenu" property. See @link{ROOT_SET}, @link{SELF_SET}, @link{OBJECT_SET}, and @link{AVATAR_SET}.	 * @param {string} [title] - The name of this action set, if used as a submenu.	 * @param {string} [desc] - The description of this action set, if used as a submenu.	 */	registerActionSet: ContextMenu_registerActionSet, 	/**	 * Unregisters an action set from the context menu.	 * @param {string} name - The name of the action set to unregister	 */	unregisterActionSet: ContextMenu_unregisterActionSet, 	/**	 * Edits a previously registered context menu action set. An action set can only be edited by the script that registered it.	 * @param {string} name - The name of the action set	 * @param {(Object.<string, ContextMenu.Action>|ContextMenu.Action[])} actions - The action information, present object keys or array indices will replace the registered ones	 */	editActionSet: ContextMenu_editActionSet, 	/*	 * Disables the context menu open action so the bound buttons can be reused.	 *	 * The context menu will stay disabled if another script	 * has disabled it and hasn't re-enabled it yet.	 *	 * NOTE: If your script uses @link{ContextMenu.disable}, you should	 * call @link{ContextMenu.enable} in @link{Script.scriptEnding}.	 */	disable: ContextMenu_disable, 	/**	 * Re-enables the context menu open action, after @link{ContextMenu.disable} has been called.	 *	 * The context menu will stay disabled if another script	 * has disabled it and hasn't re-enabled it yet.	 *	 * NOTE: If your script uses @link{ContextMenu.disable}, you should	 * call @link{ContextMenu.enable} in @link{Script.scriptEnding}.	 */	enable: ContextMenu_enable,};