Sublime Forum

How Can I Implement a Dynamic Menu?

#1

How can I implement a menu like "$syntax" or "$build_system", which can decide the number of its items dynamically, just add files in a fixed format. The function show_popup_menu is similar to it, but it is not a context menu and it dose not follow the parent menu.

0 Likes

#2

as far as I know, it’s not possible without creating a .sublime-menu file and updating it with the desired items when they change

1 Like

#3

As far as I know, the main-menu View>Syntax and Tools>Build System just with a commands "$file_types" and "$build_systems", and the items of them are added dynamically with the found resource of syntax-file and build-system-file.

0 Likes

#4

yes, those are built in to ST core I believe, not implemented from a package or plugin

0 Likes

#5

I just hope the author can provide an API that can implement this function.

0 Likes

#6

If you know about the event to trigger menu updates, it’s quite easy to create a json file and put it into ST’s Cache or Packages/User path. ST will then update seamlessly.

If you need an example, Theme-Menu-Switcher package makes use of this technology:

0 Likes

#7

Sorry you misunderstood my words, what I want is to create a sort of menu, its description is a list of string, each string is generated by a setting or a file(like the Build System menu, its items(children menu) are decided by files with extension of sublime-build and the Syntax menu, decided by files with extension of sublime-syntax), you can add and delete. Just think the scene, I made a context-menu for searching selected codes online. If no words are selected, it will not be displayed(the is_visible() function return False), when there are selected words, right-click and hover mouse on Search Online, it will provides several options (several urls), so it’s just a menu with several entry rather than several menus. Currently, I achieve similar goals through show_popup_menu, but its location does not depend on the location the menu was clicked, which is a headache.

0 Likes

#8

As you already found Sublime Text has an show_popup_menu() which displays a popup menu at the current caret position with the items you specify. But this popup menu is always a standalone popup and would overwrite the default context menu if bound to right mouse button.

But Sublime Text does not provide a direct API to dynamically extend existing menus, like the main menu, side bar context menu or tab context menu, … .

The only way to achieve something like that is to dynamically create *.sublime-menu and put them into a directory ST tracks for changes.

Example:

You have a word boomerang selected in your view and want to dynamically add an item Search for "boomerang" to the context menu of a view.

Your plugin would need an EventListener that looks for the text command context_menu which is fired if the right mouse button is clicked.

With that event your listener reads the selected text and creates a new Context.sublime-menu in a proper path. (I’d suggest ST’s Cache path rather than Packages).

Source of my_context.py

import json
import os

import sublime
import sublime_plugin


class MySearchCommand(sublime_plugin.TextCommand):

    def run(self, edit, what):
        """The command to execute via the dynamic menu item."""
        sublime.status_message("Searching {} ...".format(what))


class EventListener(sublime_plugin.EventListener):

    def on_text_command(self, view, command, args):

        # ST wants to display the context menu
        if command == "context_menu":
            
            # read the text of the first selection
            text = view.substr(view.sel()[0])

            # create the menu item to add to the context menu
            context_menu = [
                {
                    "caption": "Search for {}".format(text),
                    "command": "my_search",
                    "args": {"what": text}
                }
            ]

            # make sure cache path exists
            cache_path = os.path.join(sublime.cache_path(), "MySearchPlugin")
            os.makedirs(cache_path, exist_ok=True)

            # write the context menu item to the cache path
            with open(os.path.join(cache_path, "Context.sublime-menu"), "w+") as cache:
                cache.write(json.dumps(context_menu, cache))

While this is easy for the context menu, you’d need to find proper events to use to update the * .sublime-menu files of other menus as they are most likely not called via commands.

0 Likes

#9

Thank you very much, I don’t want to make it complicated, so I did it like this.

class SearchOnlineCommand(sublime_plugin.TextCommand):
    WHERE_SEARCH = {
        "Baidu": "https://www.baidu.com/s?ie=UTF-8&wd=%s",
        "Github": "https://github.com/search?q=%s&type=Code",
        "Google": "http://google.com/#q=%s",
        "Wiki": "https://en.wikipedia.org/wiki/%s"
    }

    def run(self, edit, event):
        settings = sublime.load_settings("ContextTips.sublime-settings")
        self.WHERE_SEARCH.update(settings.get("where_search", {}))
        self.WHERE = sorted(self.WHERE_SEARCH)
        self.view.show_popup_menu(self.WHERE, self.on_done)

    def on_done(self, index):
        if index == -1:
            return
        where = self.WHERE[index]
        webbrowser.open_new_tab(self.WHERE_SEARCH[where] % self.content)

    def get_selected(self, event):
        pt = self.view.window_to_text((event["x"], event["y"]))
        selected = self.view.sel()
        if len(selected):
            selection = selected[0]
            if not selection.empty() and selection.contains(pt):
                content = self.view.substr(selected[0]).strip()
                if content:
                    return content
        return None

    def is_visible(self, event):
        self.content = self.get_selected(event)
        return self.content is not None

    def is_enabled(self, event):
        return self.is_visible(event)

    def want_event(self):
        return True
0 Likes

#10

Thank you,I did it, although it cost more time.

import os
import json
import webbrowser

import sublime
import sublime_plugin


cache_path = os.path.join(sublime.cache_path(), __package__)
menu_file = os.path.join(cache_path, "Context.sublime-menu")


class SearchOnlineCommand(sublime_plugin.WindowCommand):
    def run(self, url):
        webbrowser.open_new_tab(url)


class EventListener(sublime_plugin.EventListener):
    WHERE_SEARCH = {}

    def get_selected(self, view, event):
        pt = view.window_to_text((event["x"], event["y"]))
        selected = view.sel()
        if len(selected):
            selection = selected[0]
            if not selection.empty() and selection.contains(pt):
                content = view.substr(selected[0]).strip()
                if content:
                    return content
        return None

    def on_post_text_command(self, view, command, args):
        if command == "context_menu":
            try:
                os.remove(menu_file)
            except:
                pass

    def on_text_command(self, view, command, args):
        if command == "context_menu":
            content = self.get_selected(view, args['event'])
            if content is None:
                return
            settings = sublime.load_settings("search_online.sublime-settings")
            self.WHERE_SEARCH.update(settings.get("where_search", {}))

            context_menu = [{
                "caption": "Search Online",
                "children": []
            }]
            for where in sorted(self.WHERE_SEARCH):
                context_menu[0]["children"].append({
                    "caption": where,
                    "command": "search_online",
                    "args":{
                        "url": self.WHERE_SEARCH[where] % content
                    }
                })

            with open(menu_file, "w+") as cache:
                cache.write(json.dumps(context_menu, cache))

I don’t know why there is some wrong with indent!

0 Likes

#11

search_online.sublime-settings:

	// where_search:
	//	a title to show at the context-menu,
	//	and a url with a "%s" to insert words.
	// such as:
	//	"Baidu": "https://www.baidu.com/s?ie=UTF-8&wd=%s",
	//	"Github": "https://github.com/search?q=%s&type=Code",
	//	"Google": "http://google.com/#q=%s",
	//	"Wiki": "https://en.wikipedia.org/wiki/%s"
	// etc.
  	"where_search": {
        "Wiki": "https://en.wikipedia.org/wiki/%s"
     }

0 Likes

#12

I was wrong, it doesn’t work!

0 Likes

#13

If I recall correctly, the code in Sublime that notices new package resource files and loads them happens (or seems to happen) in the same thread that commands and events are dispatched in.

For example, if in response to a command you write a file to a package folder, you can’t use sublime.load_resource() to load that file within the same command or some other command that command immediately executes because Sublime hasn’t noticed the new file yet.

So possibly your code doesn’t do what you want because of interplay with this;you write the menu in response to the event, then the command finishes executing before the new file is noticed (and the other event handler then deletes it)

In such a case, a potential workaround may be to append the following to your on_text_command handler:

sublime.set_timeout(lambda: view.run_command("context_menu", args), 100)
return None

That would delay the command execution for a bit to give Sublime a chance to notice the new resource and load it.

The 100 here is just a guess; it may require a bit more of a delay it perhaps it will also work with less. My vague recollection is that something in this vicinity always worked whereas smaller values sometimes did not.

0 Likes

#14

It’s broken by

def on_post_text_command(self, view, command, args):
    if command == "context_menu":
        try:
            os.remove(menu_file)
        except:
            pass

Without it, the menu item shows correctly. Guess the file is deleted to early by this event handler.

0 Likes

#15

Thank you for your response and help, I’m sorry for my poor English and expression!

I found the cache menu file is deleted after the context_menu command runs and exits. The reason for this maybe is the menu file must be created before the context menu is displayed.
If we delete the codes:

def on_post_text_command(self, view, command, args):
    if command == "context_menu":
        try:
            os.remove(menu_file)
        except:
            pass

The menu item will be shown but still some small problems, maybe because the cache menu file does not exist before the context_menu command is issued. Follow this I found a right way, that’s the codes:

import json
import os
import traceback
import webbrowser

import sublime
import sublime_plugin


def cache_path():
    return os.path.join(sublime.cache_path(), __package__)


def write_menu(menu):
    menu_path = os.path.join(cache_path(), "Context.sublime-menu")
    with open(menu_path, "w+") as cache:
        cache.write(json.dumps(menu, cache))


def plugin_loaded():
    os.makedirs(cache_path(), exist_ok=True)
    write_menu([])


def plugin_unloaded():
    try:
        os.remove(os.path.join(cache_path(), "Context.sublime-menu"))
    except:
        pass


class SearchOnlineCommand(sublime_plugin.WindowCommand):
    def run(self, url):
        webbrowser.open_new_tab(url)


class SearchOnlineListener(sublime_plugin.EventListener):
    def get_selected(self, view, event):
        pt = view.window_to_text((event["x"], event["y"]))
        selected = view.sel()
        if len(selected):
            selection = selected[0]
            if not selection.empty() and selection.contains(pt):
                content = view.substr(selected[0]).strip()
                if content:
                    return content
        return None

    def on_post_text_command(self, view, command, args):
            if command == "context_menu":
                write_menu([])

        def on_text_command(self, view, command, args):
            if command == "context_menu":
                content = self.get_selected(view, args['event'])
                if content is None:
                    return
                settings = sublime.load_settings("search_online.sublime-settings")
                where_search = settings.get("where_search", {})
                if where_search:
                    context_menu = [{
                        "caption": "Search Online",
                        "children": []
                    }]
                    for where in sorted(where_search):
                        context_menu[0]["children"].append({
                            "caption": where,
                            "command": "search_online",
                           "args": {
                                "url": where_search[where] % content
                            }
                        })
                    write_menu(context_menu)

and this is the settings:

	// where_search:
	//	a title to show at the context-menu,
	//	and a url with a "%s" to insert words.
	// such as:
	//	"Baidu": "https://www.baidu.com/s?ie=UTF-8&wd=%s",
	//	"Github": "https://github.com/search?q=%s&type=Code",
	//	"Google": "http://google.com/#q=%s",
	//	"Wiki": "https://en.wikipedia.org/wiki/%s"
	// etc.
  	"where_search": {
      	"Wiki" : "https://en.wikipedia.org/wiki/%s",
 		"Baidu" : "https://www.baidu.com/s?ie=UTF-8&wd=%s",
 		"Github": "https://github.com/search?q=%s&type=Code"
 		// "Google" : "http://google.com/#q=%s",
	}

0 Likes

#16

Looks good.

0 Likes