// 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/1767 Script.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 scripts/tutorials/contextMenu 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 true, 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 false, 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 true, 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.|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.|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, };