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.
How Can I Implement a Dynamic Menu?
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
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.
yes, those are built in to ST core I believe, not implemented from a package or plugin
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:
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.
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.
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
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!
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"
}
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.
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.
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",
}