Sublime Forum

[Solved] How to add/remove a default menu entry when a X package is/isn't enabled/installed?

#1

How to add/remove a default menu entry when a X package is/isn’t enabled/installed?

On the default Sublime Text Context.sublime-menu file there is this line, which only show on the real menu, when Goto Definition command is available:

{ "command": "context_goto_definition", "caption": "Goto Definition" },

class ContextGotoDefinitionCommand(sublime_plugin.TextCommand):
    def run(self, edit, event):
        pt = self.view.window_to_text((event["x"], event["y"]))

        symbol, locations = symbol_at_point(self.view, pt)

        navigate_to_symbol(self.view, symbol, locations)

    def is_visible(self, event):
        pt = self.view.window_to_text((event["x"], event["y"]))
        symbol, locations = symbol_at_point(self.view, pt)

        return len(locations) > 0

    def want_event(self):
        return True

Now I want to do something similar. How to enable this menu entry below, only when there is not available/enable/installed the package SyncedSideBar?

{ "command": "reveal_in_side_bar", "caption": "Reveal in Side Bar" },

I could think about a dedicated plugin to detect whether the SyncedSideBarpackage is available and insert it into the Context.sublime-menu when is is not available and to remove it when it is available. Is it the only way?

I want to this because the package SyncedSideBar became the menu entry Reveal in Side Bar not useful as it is always revealing it on the side bar.
But sometimes, you want to disable the package SyncedSideBar because it causes trouble, then the menu entry Reveal in Side Bar is necessary/useful again.

0 Likes

#2

Commands hide or disable themselves from menus by having an is_visible member that gets invoked every time a menu is about to be displayed.

As such, if you want to hide a command only when certain constraints are met you would need to implement your own command that invokes the command that you’re interested in, but returns False when asked and the command you want to invoke is not available.

Or if you will, unless someone has already created a plugin to do this, I don’t think you’re going to be able to do it without some plugin coding of your own.

1 Like

#3

Thanks @OdatNurd, good solution. I got into this code, now is need to fill some things:

  1. # call { “command”: “reveal_in_side_bar”, “caption”: “Reveal in Side Bar” },
  2. # what is called right after all plugins get loaded?
  3. # if SyncedSideBar is enabled/installed.

This is the code:

isSyncedSideBarEnabled = False

class SyncedSideBarRevealInSideBarCommand(sublime_plugin.TextCommand):

    def run(self, edit):
        # call { "command": "reveal_in_side_bar", "caption": "Reveal in Side Bar" },

    def is_visible(self, event):
        return !isSyncedSideBarEnabled


# Plugin loaded is not the good place, because SyncedSideBar may be loaded after it, 
# then what is called right after all plugins get loaded?
def plugin_loaded():

    packageControlSettings = sublime.load_settings('Package Control.sublime-settings')
    userSettings           = sublime.load_settings('Preferences.sublime-settings')

    def read_pref():
        # if SyncedSideBar is enabled/installed
        isSyncedSideBarEnabled = True
        #else
        #isSyncedSideBarEnabled = False

    # read initial setting
    read_pref()

    # listen for changes
    packageControlSettings.add_on_change("Package Control", read_pref)
    userSettings.add_on_change("Preferences", read_pref)
0 Likes

#4

The window class has a method named run_command that can do this. From a text command like you’ve created here, that would be something like:

self.view.window ().run_command ("reveal_in_side_bar")

The caption part is a part of the menu that sets the menu text, so you don’t need to worry about that.

You have the right idea in your code below. As far as I know there’s no global method to tell you that all plugins have been loaded, only that your plugin has been loaded.

This one, I’m not sure on. You can tell if a package is disabled by checking the ignored_packages preference to see if it’s in the list or not, but it not being in that list doesn’t mean that it’s installed.

Package Control maintains an installed_packages setting that lists all of the packages that it has installed, so you could check that to see if the package is installed and then check the ignored setting to see if it’s disabled. This doesn’t cover packages that were manually installed, though.

PackageControl knows how to do this (I think; I’m not in a position to verify that at the moment so I may be lying), so you might want to peruse the source for it to see how it’s doing it.

Based on what you’re trying to do that seems like it would be overkill and you could just assume that the package is installed and running unless it’s ignored, though.

2 Likes

#5

Thanks @OdatNurd, now I need to fix one stranger problem:

Traceback (most recent call last):
  File "D:\User\Dropbox\Applications\SoftwareVersioning\SublimeText\sublime_plugin.py", line 476, in is_visible_
    ret = self.is_visible()
TypeError: is_visible() missing 1 required positional argument: 'event'

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "D:\User\Dropbox\Applications\SoftwareVersioning\SublimeText\sublime_plugin.py", line 478, in is_visible_
    ret = self.is_visible()
TypeError: is_visible() missing 1 required positional argument: 'event'

My is visible seens pretty ok, what may it be?

isNotSyncedSideBarEnabled = True

class SyncedSideBarRevealInSideBarCommand(sublime_plugin.TextCommand):

    def run(self, edit):

        self.view.window ().run_command ("reveal_in_side_bar")


    def is_visible(self, event):

        return isNotSyncedSideBarEnabled


def plugin_loaded():

    packageControlSettings = sublime.load_settings('Package Control.sublime-settings')
    userSettings           = sublime.load_settings('Preferences.sublime-settings')

    def updateIsSyncedSideBarEnabled():

        print('    updateIsSyncedSideBarEnabled!!!!')

        ignoredPackages   = userSettings.get( "ignored_packages" )
        installedPackages = packageControlSettings.get( "installed_packages" )

        if any( "SyncedSideBar" in package for package in installedPackages ):

            if any( "SyncedSideBar" in package for package in ignoredPackages ):

                isNotSyncedSideBarEnabled = True

            else:

                isNotSyncedSideBarEnabled = False

        else:

            isNotSyncedSideBarEnabled = True


    def read_pref_async():

        print('READ_PREF_ASYNC!!!!')
        updateIsSyncedSideBarEnabled()


    def read_pref_package():

        print('READ_PREF_PACKAGE!!!!')
        updateIsSyncedSideBarEnabled()


    def read_pref_preferences():

        print('READ_PREF_PREFERENCES!!!!')
        updateIsSyncedSideBarEnabled()


    # read initial setting, after all packages being loaded
    sublime.set_timeout_async( read_pref_async, 10000 )

    # listen for changes
    packageControlSettings.add_on_change( "Package Control", read_pref_package )
    userSettings.add_on_change( "Preferences", read_pref_preferences )

    #print( userSettings.get( "ignored_packages" ) )
    #print( packageControlSettings.get( "installed_packages" ) )

This is the new menu entry:

{ "command": "synced_side_bar_reveal_in_side_bar", "caption": "Reveal in Side Bar" },

0 Likes

#6

The key to this is the last line:

TypeError: is_visible() missing 1 required positional argument: ‘event’

Basically, it’s telling you that your is_visible is defined to take two arguments (self and event), but when the method is called, it’s not passing a parameter for event, which is an error.

The first line tells you where the code making the call is, so it would appear that Sublime does not pass an event parameter to this method. The code doesn’t use that parameter for anything, so just removing it should solve that problem.

1 Like

#7

Thanks! It is working, I removed the event parameter as you said.

An optimization attempt

Now I am trying to do an optimization on updateIsSyncedSideBarEnabled, and to catch all cases scenarios. But the problem is, the python cannot catch the exception throwed by run_command( "side_bar_update_sync" ). The exception catching for errors like trying to call self.view, when there is no self passed by, are working fine.

RUNNING
Traceback (most recent call last):
  File "D:\User\Dropbox\Applications\SoftwareVersioning\SublimeText\sublime_plugin.py", line 538, in run_
    return self.run()
TypeError: run() missing 1 required positional argument: 'enable'
RUNNING THIS
isNotSyncedSideBarEnabled: False

This exception should be caught instead of throwed.

def updateIsSyncedSideBarEnabled():

    global isNotSyncedSideBarEnabled
    print('    updateIsSyncedSideBarEnabled!!!!')
    
    sublime.active_window().run_command( "reveal_in_side_bar" )
    
    try:

        print( 'RUNNING' )
        sublime.active_window().run_command( "side_bar_update_sync" )
        isNotSyncedSideBarEnabled = False
        print( 'RUNNING THIS' )

    except BaseException:

        isNotSyncedSideBarEnabled = True
        print( 'RUNNING THIS ALSO' )

    print( 'isNotSyncedSideBarEnabled: ' + str( isNotSyncedSideBarEnabled ) )
0 Likes

#8

I’m not sure offhand why the exception handling may not be working (I’m not super fluent in python in general), but it looks like your side_bar_update_sync method has a parameter named enable; was that intentional? (i.e. if it’s not, you could remove it and stop the exception from ever happening).

1 Like

#9

If I provide a parameter for side_bar_update_sync, is does not throw an exception. However, it is not also throwing exceptions when I provide something like side_bar_update_syncd or sadsfadsfide_asdfasdfad which is not the command it self.

This is its original context menu calling:

[
    // This is actually a sublime built-in command, on the right click menu by default
    { "caption": "Side Bar: Reveal File", "command": "reveal_in_side_bar" },

    // SyncedSideBar commands
    { "caption": "Side Bar: Enable Syncing", "command": "side_bar_update_sync", "args": { "enable": true } }, 
    { "caption": "Side Bar: Disable Syncing", "command": "side_bar_update_sync", "args": { "enable": false } 
]

This looks like a bug on the Sublime Text Python use, or one bug the python’s interpreter the Sublime Text is using. Because a exception, is a exception is should be caught for the most general classes as BaseException and Exception.

0 Likes

#10

I was going to point that out in my earlier message and forgot all about it; if the command you’re calling expects an argument you have to provide it. It wasn’t clear from your example code where that command was coming from, and it totally slipped my mind.

Sublime silently eats commands that don’t exist without displaying any apparent error, which is handy for turning off default key bindings that you don’t want to bind to anything else (for example). I assume that’s by design, though.

Does it still exhibit that behaviour if you use an except: clause without mentioning a class? Pretty sure that’s a general no-no because it catches absolutely everything, but it would be interesting to see what happens in that case.

1 Like

#11

Thanks for the infos.

It does stills throwing the exception instead of catching it:

I filled a bug on https://github.com/SublimeTextIssues/Core/issues/1359

0 Likes

#12

I finished it, this is the full code:

import sublime
import sublime_plugin



isNotSyncedSideBarEnabled = True

class SyncedSideBarRevealInSideBarCommand(sublime_plugin.TextCommand):

    global isNotSyncedSideBarEnabled

    def run(self, edit):

        self.view.window().run_command ("reveal_in_side_bar")


    def is_visible(self):

        #print( 'isNotSyncedSideBarEnabled: ' + str( isNotSyncedSideBarEnabled ) )
        return isNotSyncedSideBarEnabled



def plugin_loaded():

    global isNotSyncedSideBarEnabled

    userSettings           = sublime.active_window().active_view().settings()
    packageControlSettings = sublime.load_settings('Package Control.sublime-settings')

    def updateIsSyncedSideBarEnabled():

        #print('    updateIsSyncedSideBarEnabled!!!!')

        isIgnored = False
        isIgnored = False
        
        ignoredPackages   = userSettings.get( "ignored_packages" )
        installedPackages = packageControlSettings.get( "installed_packages" )

        if( ignoredPackages != None ):

            isIgnored = any( "SyncedSideBar" in package for package in ignoredPackages )

        if( installedPackages != None ):

            isInstalled = any( "SyncedSideBar" in package for package in installedPackages )

        updateGlobalData( isIgnored, isInstalled )

        #print( 'isIgnored: ' + str( isIgnored ) )
        #print( 'isInstalled: ' + str( isInstalled ) )


    def updateGlobalData( isIgnored, isInstalled ):

        global isNotSyncedSideBarEnabled

        if isIgnored:

            isNotSyncedSideBarEnabled = True

        else:

            if isInstalled:

                isEnabled = userSettings.get( "reveal-on-activate" )
                isNotSyncedSideBarEnabled = not isEnabled

            else:

                isNotSyncedSideBarEnabled = True

        #print( 'isNotSyncedSideBarEnabled: ' + str( isNotSyncedSideBarEnabled ) )


    def read_pref_async():

        #print('READ_PREF_ASYNC!!!!')
        updateIsSyncedSideBarEnabled()


    def read_pref_package():

        #print('READ_PREF_PACKAGE!!!!')
        packageControlSettings = sublime.load_settings('Package Control.sublime-settings')
        updateIsSyncedSideBarEnabled()


    def read_pref_preferences():

        #print('READ_PREF_PREFERENCES!!!!')
        userSettings = sublime.active_window().active_view().settings()
        updateIsSyncedSideBarEnabled()


    # read initial setting, after all packages being loaded
    sublime.set_timeout_async( read_pref_async, 10000 )

    # listen for changes
    userSettings.add_on_change( "Preferences", read_pref_preferences )
    packageControlSettings.add_on_change( "Package Control", read_pref_package )

    #print( userSettings.get( "ignored_packages" ) )
    #print( packageControlSettings.get( "installed_packages" ) )

And the menus entries placed on Packages/Default/Context.sublime-menu (The user packages folder):

    [
        { "command": "open_context_url" },
        { "command": "context_goto_definition", "caption": "Goto Definition" },
        { "caption": "-", "id": "clipboard" },
        { "command": "copy" },
        { "command": "cut" },
        { "command": "paste" },
        { "caption": "-", "id": "selection" },
        { "command": "select_all" },
        { "caption": "-", "id": "file" },
        { "command": "copy_path", "caption": "Copy File Path" },
        { "command": "open_in_browser", "caption": "Open in Browser" },
        { "command": "open_dir", "args": {"dir": "$file_path", "file": "$file_name"}, "caption": "Open Containing Folder…" },
        { "command": "synced_side_bar_reveal_in_side_bar", "caption": "Reveal in Side Bar" },
        { "caption": "-", "id": "end" }
    ]
0 Likes