SOLVED Remove Shortcut failed

Hello :

In Short :
Failed to remove a shortcut with python like : remove Shift + 1 for move camera and add it to my own tool .

More Descriptions :
I am trying to remove a C4D shortcut and add it to my own tools , but it didn't work in my plugins :
8b38a501-d142-4eef-b69b-3953302f2cdb-image.png
I want my tool has 4 shortcut , like :

  1. Ctrl + Shift + Alt + 1 : Do A
  2. Ctrl + Shift + 1 : Do B
  3. Shift + 1 : Do C
  4. Ctrl + 1 : Do D
    But some of them like Ctrl + 1 has employed . Here is little codes :
#--------------------------------------------------------------------
# Plugin Class
#--------------------------------------------------------------------
###  ==========  Execute  ==========  ###
#? 缓存灯光
class CacheLight(LightSolo):
    def __init__(self) -> None:
        pass
    # Override - Called when the plugin is selected by the user.
    def Execute(self, doc=c4d.documents.BaseDocument):
        self.CacheState()
        return True

#? 重置灯光   
class ResetLight(LightSolo):
    def __init__(self) -> None:
        pass
    # Override - Called when the plugin is selected by the user.
    def Execute(self, doc=c4d.documents.BaseDocument):
        alllights = self.getAllLights()
        for light in alllights:
            self.ResetState(light) # 重置灯光
        return True

#? 灯光组solo  
class SoloLightGroup(LightSolo):
    def __init__(self) -> None:
        pass
    # Override - Called when the plugin is selected by the user.
    def Execute(self, doc=c4d.documents.BaseDocument):
        self.SoloLightGroup() # 灯光组solo
        return True

#? 灯光solo   
class SoloLights(LightSolo):
    def __init__(self) -> None:
        pass
    # Override - Called when the plugin is selected by the user.
    def Execute(self, doc=c4d.documents.BaseDocument):
        self.SoloLights() # 灯光solo
        return True
#--------------------------------------------------------------------
# Shortcut.
#--------------------------------------------------------------------  
def RemoveShortcut(qualifier,key):
    """
    Remove Shortcut by given qualifier and key    
        
    Args:
        qualifier (int): modifier key 
        key (int): ascii code of key
    """
    for x in range(c4d.gui.GetShortcutCount()):
        shortcutBc = c4d.gui.GetShortcut(x)
        # Check if shortcut is stored in the basecontainer.
        if shortcutBc[0] == qualifier and shortcutBc[1] == key:
            index = x    
    try:
        c4d.gui.RemoveShortcut(index)
    except:
        print ("Shortcut {} remove failed".format(c4d.gui.Shortcut2String(qualifier, key)))
        return False
    
def AddShortCut(qualifier,key,pluginID):
    """
    Add Shortcut by given qualifier and key to given ID  
        
    Args:
        qualifier (int): modifier key 
        key (int): ascii code of key
        pluginID (int): plugin ID
    """
    for x in range(c4d.gui.GetShortcutCount()):
        shortcutBc = c4d.gui.GetShortcut(x)
        # Check if shortcut is stored in the basecontainer.        
        if shortcutBc[0] == qualifier and shortcutBc[1] == key:
            if shortcutBc[c4d.SHORTCUT_PLUGINID] == pluginID:
                print ("Shortcut {} is already Used for Command ID: {}".format(c4d.gui.Shortcut2String(qualifier, key), shortcutBc[c4d.SHORTCUT_PLUGINID]))
                return
        
    # Define shortcut container
    bc = c4d.BaseContainer()
    bc.SetInt32(c4d.SHORTCUT_PLUGINID, pluginID)
    bc.SetLong(c4d.SHORTCUT_ADDRESS, 0)
    bc.SetLong(c4d.SHORTCUT_OPTIONMODE, 0)
    # User defined key
    bc.SetLong(0, qualifier)
    bc.SetLong(1, key)
    return c4d.gui.AddShortcut(bc)

###  ==========  Register Plugin  ==========  ###
if __name__ == "__main__":
            
    key = 49 # num 1 next ~
    # remove shortcut reg with Cinema 4D for [move camera]
    RemoveShortcut(7,key)
    RemoveShortcut(4,key)
    RemoveShortcut(1,key)
    RemoveShortcut(3,key)    
    # Add own shortcut
    AddShortCut(7,key,SUB_PLUNIGID_CACHE)
    AddShortCut(4,key,SUB_PLUNIGID_SOLOG)
    AddShortCut(1,key,PLUNIGID)
    AddShortCut(3,key,SUB_PLUNIGID_RESET)
    c4d.EventAdd()
    # Plugins Register
    iconfile = c4d.bitmaps.BaseBitmap()
    iconfile.InitWith(ICONPATH)
    c4d.plugins.RegisterCommandPlugin(
        id = SUB_PLUNIGID_CACHE,
        str = PLUNGINNAME + "Cache State",
        info = c4d.PLUGINFLAG_HIDEPLUGINMENU, # hide
        help = "Cache All Light Objects State",
        dat = CacheLight(),
        icon = iconfile
   )
    c4d.plugins.RegisterCommandPlugin(
        id = SUB_PLUNIGID_RESET,
        str = PLUNGINNAME + "Reset State",
        info = c4d.PLUGINFLAG_HIDEPLUGINMENU, # hide
        help = "Reset All Light Objects State",
        dat = ResetLight(),
        icon = iconfile
   )
    c4d.plugins.RegisterCommandPlugin(
        id = SUB_PLUNIGID_SOLOG,
        str = PLUNGINNAME + "Solo Group",
        info = 0, # c4d.PLUGINFLAG_COMMAND_STICKY,
        help = "Solo Selected Light Objects in Group",
        dat = SoloLightGroup(),
        icon = iconfile
   )
    c4d.plugins.RegisterCommandPlugin(
        id = PLUNIGID,
        str = TITLE,
        info = c4d.PLUGINFLAG_HIDEPLUGINMENU, # hide
        help = INFO,
        dat = SoloLights(),
        icon = iconfile
   )   

Hello @dunhou,

Thank you for reaching out to us, and thank you for the clear posting, much appreciated. There are multiple reasons why removing shortcuts does not work in your example.

  1. The major reason is how you handle your index variable in RemoveShortcut(int, int).
        if shortcutBc[0] == qualifier and shortcutBc[1] == key:
            index = x  # You define #index only here.
    try:
        # Will raise an AttributeError for 'index' when the condition above never ran.
        c4d.gui.RemoveShortcut(index) 
    except:
        ...
  1. You did also not respect some of the intricacies of the shortcut containers, which is the triggering factor for your condition never being met.

Find a commented example of your RemoveShortcut at the end of the posting. I turned off the actual removal of shortcuts in the example so that running it will not break your Cinema 4D. We might also add a small example and some other documentation to our SDK for shortcuts, because currently the makeup of a shortcut container is not too well documented.

Cheers,
Ferdinand

The output:

RemoveShortcut([c4d.QUALIFIER_SHIFT, '1']) = 4
RemoveShortcut([c4d.QUALIFIER_CTRL, c4d.QUALIFIER_SHIFT, '1']) = 434
RemoveShortcut(['M', 'S']) = 194
RemoveShortcut([c4d.QUALIFIER_CTRL, 'A']) = 53
RemoveShortcut([c4d.QUALIFIER_CTRL, 'A'], managerId=nodeEditorManager) = 390
RemoveShortcut([c4d.QUALIFIER_CTRL, 'A'], pluginId=nodeEditorSelectAllNodes) = 390

The code:

"""Demonstrates how to find and remove shortcuts in Cinema 4D.

Shortcuts are tied to how Cinema 4D internally handles sequences of key strokes (called input
events in Cinema 4D). The example showcases besides the general data structures with which these
events are communicated ways to disambiguate shortcuts for different input contexts, e.g., pressing
'CTRL + A' in the object manager and 'CTRL + A' in the node editor.

I have commented out the actual removing of shortcuts int the example, so that the example can be
run without 'bricking' a Cinema 4D installation.

The example is also unable to handle mouse key shortcuts, e.g., SHIFT + ALT + LMB. It would be
entirely possible to do this, but I have fashioned RemoveShortcut() and how it interprets its
#keySequence argument in such way that this is not possible, because it distinguishes qualifier 
inputs from value inputs by type. One would have to simply make the #keySequence data structure
a bit more complex to also support mouse buttons.
"""

import typing
import c4d

def RemoveShortcut(keySequence: list[typing.Union[int, str]], 
                   managerId: typing.Optional[int] = None,
                   pluginId: typing.Optional[int] = None) -> bool:
    """
    Finds a shortcut index by the given #keySequence and optionally #managerId and/or #pluginId.

    I have commented out the actual removing of shortcuts int the example, so that the example can be
    run without 'bricking' a Cinema 4D installation.
        
    Args:
        keySequence: A sequence of keyboard inputs, e.g., [c4d.QUALIFIER_SHIFT, '1'].
        managerId (optional): The manager context of the shortcut to find or remove.
        pluginId (optional): The plugin ID of the plugin invoked by the shortcut.
    
    Returns:
        The success of the removal operation.

    Raises:
        RuntimeError: On illegal key symbols.
        RuntimeError: On non-existing shortcut key sequences.
    """
    # The input data #keySequence is fashioned in a user friendly way, e.g.,
    #
    #   [c4d.QUALIFIER_SHIFT, c4d.QUALIFIER_ALT, "S", "T"]
    #
    # for the shortcut SHIFT + ALT + S ~ T. We must bring this into a form which what aligns how
    # such data is handled internally.
    #
    #   1. All qualifiers are ORed together.
    #   2. There can be multiple successive stroke events, e.g., M ~ S.

    # The raw values of the input are,
    #
    #   [1, 4, "S", "T"]
    #
    # which are being transformed into this:
    #
    #  [  (5, 83),    # (qualifier = 1 | 4 = 5, key = ASCII_VALUE("S"))
    #     (84)        # (qualifier = 0        , key = ASCII_VALUE("T")
    #  ]

    # The list of key stroke modifier-key tuples.
    strokeData: list[tuple[int, int]] = []
    # A variable to OR together the qualifiers for the current key stroke.
    currentModifiers: int = 0

    for key in keySequence:
        # Extend a modifier key sequence, e.g., SHIFT + ALT + CTRL
        if isinstance(key, (int, float)):
            currentModifiers |= key
        # A character key was found, append an input event.
        elif isinstance(key, str) and len(key) == 1:
            strokeData.append((currentModifiers, ord(key.upper())))
            currentModifiers = 0
        # Something else was found, yikes :)
        else:
            raise RuntimeError(f"Found illegal key symbol: {key}")

    # Now we can iterate over all shortcuts in Cinema 4D.
    for index in range(c4d.gui.GetShortcutCount()):
        # Get the shortcut at #index.
        bc: c4d.BaseContainer = c4d.gui.GetShortcut(index)

        # The shortcut container #bc is structured as follows:
        #
        #   Container Access             ID     Description
        #   bc[0]                          0    Qualifier sequence of the first key stroke.
        #   bc[1]                          1    ASCII value of the first key stroke.
        #   bc[10]                        10    Qualifier sequence of the second key stroke (optional).
        #   bc[11]                        11    ASCII value of the second key stroke (optional).
        #   [...]
        #   bc[990]                      990    Qualifier sequence of the 99th key stroke (optional).
        #   bc[991]                      991    ASCII value of the 99th key stroke (optional).
        #   bc[c4d.SHORTCUT_PLUGINID]   1000    The plugin id of the thing triggered by the shortcut. 
        #   bc[c4d.SHORTCUT_ADDRESS]    1001    The the manager context of the shortcut.           
        #   bc[c4d.SHORTCUT_OPTIONMODE] 1002    If the shortcut opens the plugins options dialog.
        #
        # So, for multi keystroke events, e.g., A ~ B, the strokes are being placed with a stride
        # of 10 between the container indices [0, 990]. Also placed in this stride is qualifier OR
        # sum for each stroke. Here is the content of two real life containers for clarity:
        #
        # A container for pressing the key "0". 48 == "0". There are no qualifiers here.
        #
        #   0: 0
        #   1: 48
        #   1000: 200000084
        #   1001: 0
        #   1002: 0
        #
        # A container for pressing "M"(77) and then "S"(83).  There are no qualifiers here.
        #
        #   0: 0
        #   1: 77
        #   10: 0
        #   11: 83
        #   1000: 431000015
        #   1001: 0
        #   1002: 0

        # We test if #strokeData matches #bc.
        isMatch: bool = True
        for i, (qualifier, key) in enumerate(strokeData):
            idQualifier: int = i * 10 + 0
            idKey: int = i * 10 + 1
            # A qualifier + key stroke did not match, we break out.
            if bc[idQualifier] != qualifier or bc[idKey] != key:
                isMatch = False
                break
        
        # Something in the key sequence did not match with #strokeData, so we try the next shortcut
        # container provided by the outer loop.
        if not isMatch:
            continue
        
        # We could do here some additional tests, as shortcut key strokes do not have to be unique,
        # i.e., there could be two short-cuts "Shift + 1" bound to different manager contexts.
        if pluginId is not None and bc[c4d.SHORTCUT_PLUGINID] != pluginId:
            continue
        if managerId is not None and bc[c4d.SHORTCUT_ADDRESS] != managerId:
            continue
        
        # All tests succeeded, the shortcut at the current index should be removed, we instead just
        # return the index to make this example a bit less volatile.

        # return c4d.gui.RemoveShortcut(index)
        return index

    # All shortcuts have been traversed and no match was found, the user provided a key sequence
    # which is not a shortcut.
    raise RuntimeError(f"The shortcut sequence {keySequence} was not found.")

def main() -> None:
    """Runs the example.
    """
    # Remove/find the shortcut index for "Shift + 1"
    print (f"{RemoveShortcut([c4d.QUALIFIER_SHIFT, '1']) = }")
    # Remove/find the shortcut index for "Shift + Alt + 1"
    print (f"{RemoveShortcut([c4d.QUALIFIER_CTRL, c4d.QUALIFIER_SHIFT, '1']) = }")
    # Remove/find the shortcut index for "M ~ S"
    print (f"{RemoveShortcut(['M', 'S']) = }")

    # To filter duplicate shortcuts, either the manager context or plugin ID must be used. Most plugin
    # IDs are public, but all most no manager IDs are public. The only way to figure them out is to
    # reverse engineer the GetShortcut() containers.

    nodeEditorManager: int = 465002211    # The ID of the node editor manager.
    nodeEditorSelectAllNodes: int = 465002309  # The plugin ID of the "Select All" (nodes) command.

    # Remove/find the shortcut index for "CTR + A". There are multiple shortcuts for this sequence,
    # this will return the first match.
    print (f"{RemoveShortcut([c4d.QUALIFIER_CTRL, 'A']) = }")
    # Remove/find the shortcut index for "CTR + A" in the Node Editor context. 
    print (f"{RemoveShortcut([c4d.QUALIFIER_CTRL, 'A'], managerId=nodeEditorManager) = }")
    # Remove/find the shortcut index for "CTR + A" which invokes the plugin with the ID 465002309.
    print (f"{RemoveShortcut([c4d.QUALIFIER_CTRL, 'A'], pluginId=nodeEditorSelectAllNodes) = }")

if __name__ == "__main__":
    main()

@ferdinand
Thank you for detailed explain . It's great to add a shortcut container document update and small examples , actually , more examples on sdk or Github is really helpful.

After reading I try to re-write it and add some functions like add shortcut or check plugin's shortcut list, It's all worked as expected . Shortcut BaseContainer explain is that great to move on .

By the way , from those examples , I learned and try to keep code style as easy to read and repair like yours at the same time . So much appreciated.👏