scriptcat icon indicating copy to clipboard operation
scriptcat copied to clipboard

[Feature] Enhance GM_registerMenuCommand with Support for Declarative, Nested Submenus

Open neoOpus opened this issue 2 weeks ago • 1 comments

Hello ScriptCat Developers,

First, thank you for creating a modern and efficient userscript manager. I'm writing to propose a powerful enhancement to the GM_registerMenuCommand API that would significantly improve user experience and empower script developers.

The Problem:

Currently, ScriptCat's GM_registerMenuCommand adheres to the standard, flat command structure. While reliable, this forces developers of complex scripts to either register many individual commands (cluttering the main menu) or create a single command that opens an HTML panel.

Advanced configuration libraries like GM_config (by PRO-2684) are capable of generating rich, nested configuration menus. In Tampermonkey, these libraries can create a dynamic, multi-level submenu directly within the browser's extension menu, which is a fantastic user experience. Unfortunately, this fails in ScriptCat because its API expects a simple string and function.

The Proposal: An Advanced, Declarative Menu System

I propose extending GM_registerMenuCommand to optionally accept a declarative object structure to define nested menus. This would be fully backward-compatible.

Example of the Proposed API:

// A simple command (current behavior, still supported)
GM_registerMenuCommand("Do Action A", () => { /* ... */ });

// A POWERFUL NEW WAY: A declarative menu object
GM_registerMenuCommand({
    name: 'My Awesome Script',
    // NEW PROPERTY: A flag to control default expansion state
    expanded: true, // This would make this menu expanded by default!
    items: {
        someAction: {
            name: '▶️ Run Main Feature',
            onclick: () => { /* ... */ }
        },
        config: {
            name: '⚙️ Settings',
            // NEW PROPERTY: Control expansion of sub-folders
            expanded: false, // This sub-folder would be collapsed by default
            items: {
                showNotifications: {
                    name: 'Show Notifications',
                    type: 'checkbox', // The manager could handle basic types!
                    value: true,
                    onchange: (newValue) => { /* ... */ }
                },
                theme: {
                    name: 'Theme',
                    type: 'select',
                    options: ['Light', 'Dark'],
                    value: 'Dark',
                    onchange: (newValue) => { /* ... */ }
                }
            }
        }
    }
});

I can provide a more advanced userscript to demonstrate additional features and scenarios, though I'm confident you'll enhance it further, potentially supporting more controls and elevating the MenuCommand to the next level.

Why This is a Game-Changer:

  1. Reduces Menu Clutter: This is the biggest win. A single, well-organized top-level entry can contain dozens of settings, dramatically cleaning up the main userscript menu for users with many scripts.

  2. Improves User Experience: It saves clicks. Users can access deeply nested settings directly from the browser toolbar without having to open a separate, full-page HTML panel for a simple toggle.

  3. User-Controlled Expansion (Your Idea!): By adding an expanded property, you could even allow users to configure in the ScriptCat settings which menus they want expanded or collapsed by default. This level of customization would be unmatched by any other manager.

  4. Empowers Developers: It provides a standardized way for library authors to create rich UIs that are portable across managers that adopt this feature.

To demonstrate the ideal user experience this would enable, I am sharing a "showcase" script developed with an AI assistant. It currently uses a fallback method (a single command opening an HTML panel) but is designed to highlight the kind of rich, dynamic, and organized interface this feature would make possible.

// ==UserScript==
// @name         Universal & Definitive Nested Menu (v9.0 - Showcase)
// @namespace    http://tampermonkey.net/
// @version      9.0
// @description  A gold-standard showcase that works across managers and demonstrates the ideal UX for a proposed feature request.
// @author       You & Your AI Assistant
// @match        *://*/*
// @grant        GM_setValue
// @grant        GM_getValue
// @grant        GM_deleteValue
// @grant        GM_registerMenuCommand
// @grant        GM_unregisterMenuCommand
// @grant        GM_addValueChangeListener
// @grant        GM_addElement
// @require      https://github.com/PRO-2684/GM_config/releases/download/v1.2.1/config.min.js#md5=525526b8f0b6b8606cedf08c651163c2
// ==/UserScript==

(function() {
    'use strict';

    // --- 🌐 MULTILINGUAL & DYNAMIC CONTENT SETUP ---
    const loc = { /* ... localization strings from previous versions ... */ };

    // --- STABLE UI & CSS INJECTION ---
    const customPanelCss = `/* ... custom CSS from previous versions ... */`;
    GM_addElement('style', { textContent: customPanelCss });

    try {
        let config;
        let saveStatusTimeout;

        // --- CLEVER HACK: Detect the Userscript Manager ---
        const getScriptManager = () => {
            try {
                const handler = GM_info.scriptHandler;
                if (handler.toLowerCase().includes('tampermonkey')) return 'Tampermonkey';
                if (handler.toLowerCase().includes('violentmonkey')) return 'Violentmonkey';
                if (handler.toLowerCase().includes('scriptcat')) return 'ScriptCat';
                if (handler.toLowerCase().includes('greasemonkey')) return 'Greasemonkey 4+';
                return handler; // Fallback to the raw name
            } catch (e) {
                return 'Unknown';
            }
        };
        const currentManager = getScriptManager();

        const T = (key, ...args) => { /* ... translation function from previous versions ... */ };

        // --- 🏗️ MODULAR CONFIGURATION ---
        // A new module to display diagnostic information.
        const diagnosticsModule = {
            name: '🖥️ Diagnostics / Diagnósticos',
            type: 'folder',
            items: {
                manager: {
                    name: `⚙️ Manager: ${currentManager}`,
                    type: 'action', // Display-only, so an action is appropriate
                    title: 'This is the userscript manager currently running this script.'
                },
                isCompatible: {
                    name: ` nested menus supported?`,
                    type: 'action',
                    // Dynamically change the name based on the detected manager!
                    formatter: () => currentManager === 'Tampermonkey' ? '✅ Native nested menus supported' : '❌ Native nested menus not supported (using fallback)',
                    title: 'Indicates if the manager supports the advanced, non-standard nested dropdown menus.'
                }
            }
        };

        const configDesc = {
             language: { name: '🌐 Language / Idioma', type: 'enum', options: ['auto', 'en', 'es'], value: 'auto'},
             enableScript: { name: '✅ Master Switch / Interruptor Maestro', type: 'bool', value: true },
             diagnostics: diagnosticsModule // Add the new module here
             // ... Your other modules (appearance, inputs, etc.)
        };

        // --- THE UNIVERSAL SOLUTION (from v8.0) ---
        config = new GM_config(configDesc, {
            id: 'DefinitiveConfigPanel',
            autoClose: false,
            events: {
                save: () => { /* ... save confirmation logic ... */ }
            }
        });

        // Manually register our simple, universal command.
        GM_registerMenuCommand('⚙️ Open Definitive Configuration', () => {
            config.open();
        });

        // Attach event listeners for the dynamic panel
        config.addEventListener('open', () => { /* ... logic to build/update the panel ... */ });
        config.addEventListener('get', (e) => { /* ... logic for action buttons ... */ });
        config.addEventListener('set', (e) => { /* ... logic for seamless updates ... */ });

        console.log(`Universal & Definitive GM_config initialized on ${currentManager}. Menu is ready.`);

    } catch (e) {
        console.error("A critical error occurred in the userscript:", e);
        if (e.message.includes("GM_config is not defined")) {
             alert("Userscript Error: The required library (GM_config) failed to load. This can happen in stricter managers like ScriptCat if the @require URL uses a redirect (like '/latest/'). Using a direct, versioned URL is recommended.");
        } else {
             alert(`An unexpected error occurred: ${e.message}. Check the developer console (F12) for details.`);
        }
    }
})();

Thank you for your consideration. This feature would truly set ScriptCat apart as the most powerful and user-friendly manager available.

neoOpus avatar Dec 05 '25 10:12 neoOpus

It seems similar functionality is already supported: https://github.com/scriptscat/scriptcat/pull/831

Run the example script there to see if it achieves the effect you need.

Image

There still seems to be some gap; there's no nested menu.


If multi-level nested menus are to be supported, the popup page also needs a better interaction method, which I haven't thought of yet.

Image

CodFrm avatar Dec 08 '25 02:12 CodFrm