How to modify a key and register another key at the same time in an addon?

I’m trying to add a menu over the existing Shift S menu
for this I need to modify the existing Shift S key, from click to click and drag
and create a new Shift S key when clicking to open my menu
But I’m not able to do both at once when registering the addon
And get the default value when unregistering the addon
this is exactly how Tab is working when you enable an option to toggle mode when just click or open the pie menu when click and drag.
If someone can provide a precise example it would be great

If you want to edit existing keymap items, you should use wm.keyconfigs.active. This ensures that your addon can override the currently active keymap in Blender.

You should also disable a keymap item (and add your own via wm.keyconfigs.addon) instead of modifying it.

wm.keyconfigs.user is a bit hard to work with. I recommend to avoid using it to add or modify keymaps.

This example searches for a specific keymap based on traits like its identifier, type, value, and modifier keys. If a keymap is found based on the criteria, it gets disabled and added to a list (so you can re-enable the keymap item when the addon is unregistered).

import bpy

disabled_kmis = []

# Find a keymap item by traits.
# Returns None if the keymap item doesn't exist.
def get_active_kmi(space: str, **kwargs) -> bpy.types.KeyMapItem:
    kc = bpy.context.window_manager.keyconfigs.active
    km = kc.keymaps.get(space)
    if km:
        for kmi in km.keymap_items:
            for key, val in kwargs.items():
                if getattr(kmi, key) != val and val is not None:
                    break
            else:
                return kmi

def disable_shift_s_snap_kmi():
    # Be explicit with modifiers shift/ctrl/alt so we don't
    # accidentally disable a different keymap with same traits.
    kmi = get_active_kmi("3D View",
                         idname="wm.call_menu_pie",
                         type='S',
                         shift=True,
                         ctrl=False,
                         alt=False)

    if kmi is not None:
        kmi.active = False
        disabled_kmis.append(kmi)

if __name__ == "__main__":
    disable_shift_s_snap_kmi()
    
    # Register your own version of Shift S with click drag
    # just like you would with any addon hotkey.
    ...
    

To re-enable the disabled keymap items when the addon is uninstalled:

def register():
    for kmi in disabled_kmis:
        kmi.active = True
    
    ...
1 Like

I thought it was working but it didn’t hold after the restart and save (snap was enable again)
I did this

disabled_kmis = []

# Find a keymap item by traits.
# Returns None if the keymap item doesn't exist.
def get_active_kmi(space: str, **kwargs) -> bpy.types.KeyMapItem:
    kc = bpy.context.window_manager.keyconfigs.active
    km = kc.keymaps.get(space)
    if km:
        for kmi in km.keymap_items:
            for key, val in kwargs.items():
                if getattr(kmi, key) != val and val is not None:
                    break
            else:
                return kmi

def disable_shift_s_snap_kmi():
    # Be explicit with modifiers shift/ctrl/alt so we don't
    # accidentally disable a different keymap with same traits.
    kmi = get_active_kmi("3D View",
                         idname="wm.call_menu_pie",
                         type='S',
                         shift=True,
                         ctrl=False,
                         alt=False)
    print('kmi',kmi)
    if kmi is not None:
        kmi.active = False
        disabled_kmis.append(kmi)

addon_keymaps = []
classes = (Clear_cursor,)


def register():

    for c in classes:
        bpy.utils.register_class(c)
        
    disable_shift_s_snap_kmi()

    wm = bpy.context.window_manager
    kc = wm.keyconfigs.addon
    if kc is not None:
        km = kc.keymaps.new(name = '3D View Generic', space_type = 'VIEW_3D')

        kmi = km.keymap_items.new(idname='cursor.clear', type='NUMPAD_ASTERIX', value='PRESS')
        kmi.properties.loc = True
        addon_keymaps.append((km, kmi))
        kmi = km.keymap_items.new(idname='cursor.clear', type='NUMPAD_ASTERIX', value='PRESS', ctrl=True)
        kmi.properties.loc = False
        addon_keymaps.append((km, kmi))


def unregister():

    for kmi in disabled_kmis:
        kmi.active = True

    wm = bpy.context.window_manager
    kc = wm.keyconfigs.addon
    if kc is not None:
        for km, kmi in addon_keymaps:
            km.keymap_items.remove(kmi)

Ah, yes. This is the part I was hoping to avoid.

Blender does not guarantee that every keymap is ready when an addon is being registered. You can test this yourself by adding this to disable_shift_s_snap_kmi:


    if kmi is not None:
        kmi.active = False
        disabled_kmis.append(kmi)

    # Sanity check that "3D View Generic" actually exists
    else:
        kc = bpy.context.window_manager.keyconfigs.active
        assert "3D View Generic" in kc.keymaps  # <---------------

The assert may fail when Blender loads the first time (with the addon installed), because of the reason stated above, the keymap is probably not ready. Some keymaps will have loaded already, others won’t.

To work around this, you need to defer the addon registration until the keymap is ready.


def register():

    # --------- Start ---------
    wm = bpy.context.window_manager
    if "3D View Generic" not in wm.keyconfigs.active.keymaps:
        return bpy.app.timers.register(register, first_interval=0.1)
    # --------- End ---------


    # Continue as usual..
    disable_shift_s_snap_kmi()

    wm = bpy.context.window_manager
    kc = wm.keyconfigs.addon
    if kc is not None:
        ...

Edit:
Note that this approach will work in most cases, but you should use a fallback for times when the keymap for some reason doesn’t exist, like giving it a retry of 10 times and afterwards print a message with reason, then abort.

1 Like

when I tried to overwrite the default snap key (shift S), I deleted it, in the past. so I created a new one. doing the first previous script. the original key was reappearing! and was disabled, by the script. like if it fixed the key to his default value. I tried to do the same over J (another script I did) in edit mode, to have a menu on click and a menu when dragging. but when I install the same addon now, I have twice the same first key installed (??) and on the default config (factory settings) I get two different keys installed and no bug. but the disabled one, is still not staying after a restart with the last script. so I guess my keymap is corrupted or the userpref.blend?
here is the script of multijoin (in edit mode, J) working on a default config but not after a restart

import bpy
import bmesh
from bpy.types import Menu
from bpy.props import FloatProperty, BoolProperty
from functools import reduce

'''
-Press J: 

    -advanced_join:
    compared to a simple join (vert_connect_path),
    it can also join vertices with no face between
    it can fill faces and merge vertices (threshold) 

-Press J and move to enter pie menu:
    
    -multijoin: need a last selected vertex
     it can fill faces
    
    -slide and join: need 2 last vert or 1 last edge
     it can fill faces and merge vertices (threshold)   
    
'''


bl_info = {
    "name": "Multijoin_Pie_Menu",
    "author": "1C0D",
    "version": (1, 2, 8),
    "blender": (2, 83, 0),
    "location": "View3D",
    "description": "Normal Join, Multijoin at last, slide and join",
    "category": "Mesh",
}


def is_border_vert(vert):
    borderEdges = [edge for edge in vert.link_edges if len(edge.link_faces) == 1]
    return len(borderEdges) > 1

def are_border_verts(verts):
    return all(is_border_vert(vert) for vert in verts) 
    

class ADVANCED_OT_JOIN(bpy.types.Operator):
    """Tooltip"""
    bl_idname = "advanced.join"
    bl_label = "Advanced join"
    bl_options = {"REGISTER", "UNDO"}
    
    add_faces:BoolProperty(default=False)
    rmv_doubles_threshold: FloatProperty(
        name="Threshold", default=0.0001, precision=4, step=0.004, min=0)

    def execute(self, context):

        obj = bpy.context.object
        me = obj.data
        bm = bmesh.from_edit_mesh(me)
        ordered_selection = []
        
        try:
            for item in bm.select_history:
                if not isinstance(item, bmesh.types.BMVert):
                    raise AttributeError                    
                ordered_selection.append(item)

        except AttributeError:
            self.report({'ERROR'}, "Select Vertices only")
            return {'CANCELLED'}

        while len(ordered_selection)>1:

            for v in bm.verts:
                v.select = False 

            v1 = ordered_selection[-1]
            v2 = ordered_selection[-2]

            verts=[v1, v2]

            for v in verts:
                v.select = True

            if are_border_verts(verts):

                if self.add_faces:
                    other_verts = [e1.other_vert(v1) 
                                    for e1 in v1.link_edges for e2 in v2.link_edges 
                                        if e1.other_vert(v1) == e2.other_vert(v2)]
                    if other_verts:
                        try:
                            new=bm.faces.new([v1,other_verts[0],v2])
                        except:
                            pass
                        try:
                            new1=bm.faces.new([v1,other_verts[1],v2])
                        except:
                            pass

                        if new or new1:
                            bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
                         
                    else:
                        try:
                            bm.edges.new(verts)
                        except ValueError:
                            pass

                else: #trick can be border even if face but will try connect path between before
                    try:
                        bpy.ops.mesh.vert_connect_path()
                    except:
                        try:
                            bm.edges.new(verts)
                        except ValueError:
                            pass

            else:
                try:
                    bpy.ops.mesh.vert_connect_path()
                except:
                    pass

            ordered_selection.pop()

        bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=self.rmv_doubles_threshold)  
        bm.normal_update()
        bmesh.update_edit_mesh(me)

        return {'FINISHED'}


class SLIDE_OT_JOIN(bpy.types.Operator):
    """Slide and Join"""
    bl_idname = "join.slide"
    bl_label = "Slide and Join"
    bl_options = {"UNDO", "REGISTER"}

    rmv_doubles_threshold: FloatProperty(
        name="Threshold", default=0.0001, precision=4, step=0.004, min=0)

    def execute(self, context):

        obj = bpy.context.active_object
        bm = bmesh.from_edit_mesh(obj.data)
        bm.normal_update()

        bm.verts.ensure_lookup_table()
        history = bm.select_history[:]

        sel = [v.index for v in bm.verts if v.select]
        sel_count = len(sel)

        if sel_count > 3:
            try:
                V0 = history[-1]
                V1 = history[-2]  # 2last verts
                if not isinstance(V0, bmesh.types.BMVert):
                    raise IndexError
                if not isinstance(V1, bmesh.types.BMVert):
                    raise IndexError

            except IndexError:
                self.report({'WARNING'}, "Need 2 last vertices")
                return {'CANCELLED'}

            v0id = V0.index  # after subdiv index will be needed because history changed
            v1id = V1.index

            vertlist = []  # all vert selected except 2 last one
            vertlistid = []

            bm.verts.ensure_lookup_table()

            vertlist = [v for v in bm.verts
                        if (v.select and v != V0 and v != V1)]
            vertlist = vertlist[:]

# find extrem in vertlist

            v_double_count = [v for v in vertlist
                              for e in v.link_edges if e.other_vert(v) in vertlist]
            extremcount = [(v.index, v)
                           for v in v_double_count if v_double_count.count(v) < 2]
            try:
                E0, E1 = extremcount[:]
            except ValueError:
                self.report({'WARNING'}, "Invalid selection")
                return {'CANCELLED'}


# connect V0V1 if not

            bmesh.ops.connect_verts(bm, verts=[V0, V1])

            try:
                bm.edges.new([V1, V0])
            except:
                pass

# delete faces to have no doubles after

            for v in vertlist:
                for f in V0.link_faces:
                    if V1 in f.verts and v in f.verts:
                        f.select = True
                        break

            faces = [f for f in bm.faces if f.select]
            bmesh.ops.delete(bm, geom=faces, context='FACES_ONLY')

# connect extrems and V0 V1 if not

            if (E0[1].co - V0.co).length <= (E0[1].co - V1.co).length:

                bmesh.ops.connect_verts(bm, verts=[E0[1], V0])
                try:
                    bm.edges.new([E0[1], V0])
                except:  # ValueError:
                    pass
                bmesh.ops.connect_verts(bm, verts=[E1[1], V1])
                try:
                    bm.edges.new([E1[1], V1])
                except:
                    pass
            else:
                bmesh.ops.connect_verts(bm, verts=[E0[1], V1])
                try:
                    bm.edges.new([E0[1], V1])
                except:
                    pass
                bmesh.ops.connect_verts(bm, verts=[E1[1], V0])
                try:
                    bm.edges.new([E1[1], V0])
                except:
                    pass

# subdiv and get new verts

            for v in bm.verts:
                v.select = False
            V0.select = True  # select edge v0v1
            V1.select = True
            bm.select_flush_mode()

            edges = [e for e in bm.edges if e.select]
            newmesh = bmesh.ops.subdivide_edges(
                bm, edges=edges, cuts=sel_count-4)

            newid = []  # get id new vertices

            bm.verts.ensure_lookup_table()

            for i in newmesh['geom_split']:
                if type(i) == bmesh.types.BMVert:
                    newid.append(i.index)

# Add faces

            bm.verts.ensure_lookup_table()
            allvertid = sel+newid

            for i in allvertid:
                bm.verts[i].select = True

            bm.select_flush_mode()

            V0 = bm.verts[v0id]
            V1 = bm.verts[v1id]

            bm.edges.ensure_lookup_table()

            v2 = None
            for e in V0.link_edges:  # deselect adjacent edges
                v2 = e.other_vert(V0)
                if v2.index == E0[0] or v2.index == E1[0]:
                    e.select = False
                    break

            v2 = None
            for e in V1.link_edges:
                v2 = e.other_vert(V1)
                if v2.index == E0[0] or v2.index == E1[0]:
                    e.select = False
                    break

            edges1 = [e for e in bm.edges if e.select]

            try:
                bmesh.ops.bridge_loops(bm, edges=edges1)  # bridge loops

            except RuntimeError:
                self.report({'WARNING'}, "Need 2 edges loops")
                return {'CANCELLED'}
# Next selection

            bpy.ops.mesh.select_all(action='DESELECT')

            V0.select = True
            V1.select = True
            for i in newid:
                bm.verts[i].select = True  # ok

            bm.verts.ensure_lookup_table()

            v2 = None
            othervertid = []
            othervert = []
            next = []
#            bm.verts.ensure_lookup_table()
            for e in V0.link_edges:
                v2 = e.other_vert(V0)

                if v2.index in vertlist:
                    continue
                if v2.index in newid:
                    continue
                if v2 == V1:
                    continue
                else:
                    next.append(v2)

            v3 = None
            for e in V1.link_edges:
                v3 = e.other_vert(V1)

                if v3.index in vertlist:
                    continue
                if v3.index in newid:
                    continue
                if v3 == V0:
                    continue
                else:
                    next.append(v3)

            for v in next:
                if v.index in newid:
                    continue
                if v.index in vertlist:
                    continue
                for f in v.link_faces:
                    if not V0 in f.verts:
                        continue
                    if not V1 in f.verts:
                        continue
                    else:
                        v.select = True
                        bm.select_history.add(v)

            bmesh.ops.remove_doubles(
                bm, verts=bm.verts, dist=self.rmv_doubles_threshold)
            bmesh.ops.dissolve_degenerate(bm, edges=bm.edges, dist=0.0001)
            bmesh.update_edit_mesh(obj.data)

            return {'FINISHED'}

        else:
            self.report({'WARNING'}, "select more vertices")
            return {'CANCELLED'}


class MULTI_OT_JOIN1(bpy.types.Operator):
    """Tooltip"""
    bl_idname = "multi.join"
    bl_label = "Multijoin"
    bl_options = {"UNDO"}

    def execute(self, context):  # "selected objects"

        obj = bpy.context.object
        me = obj.data
        bm = bmesh.from_edit_mesh(me)
        bm.normal_update()
        bm.verts.ensure_lookup_table()
        actvert = bm.select_history.active
        if (isinstance(actvert, bmesh.types.BMVert)):
            vertlist = []
            for v in bm.verts:
                if v.select:
                    if not(v == actvert):
                        vertlist.append(v)

            # do connection
            for v in vertlist:
                for f in actvert.link_faces:
                    if v in f.verts:
                        # when already face: split it
                        bmesh.ops.connect_verts(bm, verts=[v, actvert])

            for v in vertlist:
                v2 = None
                for e in v.link_edges:
                    v2 = e.other_vert(v)
                    if v2 in vertlist:
                        already = False
                        for f in actvert.link_faces:
                            if v in f.verts and v2 in f.verts:
                                already = True
                                break
                        # if no face already between first and selected vert: make it
                        if not(already):
                            bm.faces.new([v, actvert, v2])

            bmesh.ops.dissolve_degenerate(bm, edges=bm.edges, dist=0.0001)

            face_sel = [f for f in bm.faces if f.select]
            bmesh.ops.recalc_face_normals(bm, faces=face_sel)

            bmesh.update_edit_mesh(me)
            
            return {'FINISHED'}

        else:

            self.report({'WARNING'}, "No last selected vertex")

        return {'CANCELLED'}


class MULTIJOIN_MT_MENU (Menu):
    bl_label = ""

    def draw(self, context):
        layout = self.layout

        pie = layout.menu_pie()

        pie.operator("multi.join", text="Multi Join")
        pie.operator("join.slide", text="Slide and Join")
        pie.operator("mesh.vert_connect", text="Connect vert pairs")
        pie.operator("mesh.vert_connect_path", text="default join")

disabled_kmis = []

def get_active_kmi(space: str, **kwargs) -> bpy.types.KeyMapItem:
    kc = bpy.context.window_manager.keyconfigs.active
    km = kc.keymaps.get(space)
    if km:
        for kmi in km.keymap_items:
            for key, val in kwargs.items():
                if getattr(kmi, key) != val and val is not None:
                    break
            else:
                return kmi

def disable_shift_s_snap_kmi():
    # Be explicit with modifiers shift/ctrl/alt so we don't
    # accidentally disable a different keymap with same traits.
    kmi = get_active_kmi("Mesh",
                         idname="mesh.vert_connect_path",
                         type='J',
                         shift=False,
                         ctrl=False,
                         alt=False)
                         
    if kmi is not None:
        kmi.active = False
        disabled_kmis.append(kmi)


addon_keymaps = []

classes = (SLIDE_OT_JOIN, MULTI_OT_JOIN1, MULTIJOIN_MT_MENU, ADVANCED_OT_JOIN)


def register():

    for cls in classes:
        bpy.utils.register_class(cls)

    wm = bpy.context.window_manager
    if "Mesh" not in wm.keyconfigs.active.keymaps:
        return bpy.app.timers.register(register, first_interval=0.1)

    disable_shift_s_snap_kmi()

    wm = bpy.context.window_manager
    kc = wm.keyconfigs.addon
    if kc:
        km = kc.keymaps.new(name='Mesh')
        kmi = km.keymap_items.new(idname='advanced.join', type='J', value='CLICK')
        addon_keymaps.append((km, kmi))
        kmi = km.keymap_items.new(idname='wm.call_menu_pie', type='J', value='CLICK_DRAG')
        kmi.properties.name = "MULTIJOIN_MT_MENU"
        addon_keymaps.append((km, kmi))


def unregister():

    for cls in classes:
        bpy.utils.unregister_class(cls)
        
    for kmi in disabled_kmis:
        kmi.active = True

    wm = bpy.context.window_manager
    kc = wm.keyconfigs.addon
    if kc is not None:
        for km, kmi in addon_keymaps:
            km.keymap_items.remove(kmi)
            
    addon_keymaps.clear()

It’s important that this:

def register():

    for cls in classes:
        bpy.utils.register_class(cls)

    wm = bpy.context.window_manager
    if "Mesh" not in wm.keyconfigs.active.keymaps:
        return bpy.app.timers.register(register, first_interval=0.1)

… becomes this:

def register():

    # This part must go first.
    wm = bpy.context.window_manager
    if "Mesh" not in wm.keyconfigs.active.keymaps:
        return bpy.app.timers.register(register, first_interval=0.1)

    for cls in classes:
        bpy.utils.register_class(cls)

If Mesh doesn’t exist, register() must run again. And if you already registered the classes once, it will attempt to register them again.

After changing the order, I can’t find any issues with the script.


Keymaps and keymap items are generated from file every time Blender starts. Blender first checks which keyconfig is active in userpref.blend, and loads the appropriate one. It’s almost impossible to make the active/default keyconfigs corrupt, but it’s possible to do this to wm.keyconfigs.user because it’s saved in userpref.blend.

I’ve tested the script and it seems to work fine.
Here’s what the keymaps are showing:
image

image

1 Like

yes my userpref.blend was corrupted, because I used wm.keyconfigs.user to do some modification before. I even lost all my keys at once, but now I understand this is about userpref.blend and not the keymap. so if it is working, this is a great news. I will fix things and try this.
big thanks.
BTW, I wonder, is Blender dealing right with shortcuts?

I must be damned. I do this and I get still the shortcut vertex connect path reenabled

def register():

    # This part must go first.
    wm = bpy.context.window_manager
    if "Mesh" not in wm.keyconfigs.active.keymaps:
        return bpy.app.timers.register(register, first_interval=0.1)

    for cls in classes:
        bpy.utils.register_class(cls)

    disable_shift_s_snap_kmi()

    wm = bpy.context.window_manager
    kc = wm.keyconfigs.addon
    if kc:
        km = kc.keymaps.new(name='Mesh')
        kmi = km.keymap_items.new(idname='advanced.join', type='J', value='CLICK')
        addon_keymaps.append((km, kmi))
        kmi = km.keymap_items.new(idname='wm.call_menu_pie', type='J', value='CLICK_DRAG')
        kmi.properties.name = "MULTIJOIN_MT_MENU"
        addon_keymaps.append((km, kmi))


def unregister():

    for cls in classes:
        bpy.utils.unregister_class(cls)
        
    for kmi in disabled_kmis:
        kmi.active = True

    wm = bpy.context.window_manager
    kc = wm.keyconfigs.addon
    if kc is not None:
        for km, kmi in addon_keymaps:
            km.keymap_items.remove(kmi)
            
    addon_keymaps.clear()

to be sure I installed the last version 2.93.5 deleted all 2.93 folders prefs

Just downloaded and tested, it seems 2.93.5 is different from 3.1. The keymap item itself isn’t ready, but the keymap which contains it is.

You can work around this by checking for the keymap item specifically, and use that as the predicate for the timer.

I’ve included the whole script and the appropriate changes to accomodate a more surefire approach.

Note that in case all attempts to disable the keymap fails, it would generally mean the keymap doesn’t exist at all. If this is the case, it’s safe to assume that there’s no keymap item to disable.

import bpy
import bmesh
from bpy.types import Menu
from bpy.props import FloatProperty, BoolProperty
from functools import reduce

'''
-Press J: 

    -advanced_join:
    compared to a simple join (vert_connect_path),
    it can also join vertices with no face between
    it can fill faces and merge vertices (threshold) 

-Press J and move to enter pie menu:
    
    -multijoin: need a last selected vertex
     it can fill faces
    
    -slide and join: need 2 last vert or 1 last edge
     it can fill faces and merge vertices (threshold)   
    
'''


bl_info = {
    "name": "Multijoin_Pie_Menu",
    "author": "1C0D",
    "version": (1, 2, 8),
    "blender": (2, 83, 0),
    "location": "View3D",
    "description": "Normal Join, Multijoin at last, slide and join",
    "category": "Mesh",
}


def is_border_vert(vert):
    borderEdges = [edge for edge in vert.link_edges if len(edge.link_faces) == 1]
    return len(borderEdges) > 1

def are_border_verts(verts):
    return all(is_border_vert(vert) for vert in verts) 
    

class ADVANCED_OT_JOIN(bpy.types.Operator):
    """Tooltip"""
    bl_idname = "advanced.join"
    bl_label = "Advanced join"
    bl_options = {"REGISTER", "UNDO"}
    
    add_faces:BoolProperty(default=False)
    rmv_doubles_threshold: FloatProperty(
        name="Threshold", default=0.0001, precision=4, step=0.004, min=0)

    def execute(self, context):

        obj = bpy.context.object
        me = obj.data
        bm = bmesh.from_edit_mesh(me)
        ordered_selection = []
        
        try:
            for item in bm.select_history:
                if not isinstance(item, bmesh.types.BMVert):
                    raise AttributeError                    
                ordered_selection.append(item)

        except AttributeError:
            self.report({'ERROR'}, "Select Vertices only")
            return {'CANCELLED'}

        while len(ordered_selection)>1:

            for v in bm.verts:
                v.select = False 

            v1 = ordered_selection[-1]
            v2 = ordered_selection[-2]

            verts=[v1, v2]

            for v in verts:
                v.select = True

            if are_border_verts(verts):

                if self.add_faces:
                    other_verts = [e1.other_vert(v1) 
                                    for e1 in v1.link_edges for e2 in v2.link_edges 
                                        if e1.other_vert(v1) == e2.other_vert(v2)]
                    if other_verts:
                        try:
                            new=bm.faces.new([v1,other_verts[0],v2])
                        except:
                            pass
                        try:
                            new1=bm.faces.new([v1,other_verts[1],v2])
                        except:
                            pass

                        if new or new1:
                            bmesh.ops.recalc_face_normals(bm, faces=bm.faces)
                         
                    else:
                        try:
                            bm.edges.new(verts)
                        except ValueError:
                            pass

                else: #trick can be border even if face but will try connect path between before
                    try:
                        bpy.ops.mesh.vert_connect_path()
                    except:
                        try:
                            bm.edges.new(verts)
                        except ValueError:
                            pass

            else:
                try:
                    bpy.ops.mesh.vert_connect_path()
                except:
                    pass

            ordered_selection.pop()

        bmesh.ops.remove_doubles(bm, verts=bm.verts, dist=self.rmv_doubles_threshold)  
        bm.normal_update()
        bmesh.update_edit_mesh(me)

        return {'FINISHED'}


class SLIDE_OT_JOIN(bpy.types.Operator):
    """Slide and Join"""
    bl_idname = "join.slide"
    bl_label = "Slide and Join"
    bl_options = {"UNDO", "REGISTER"}

    rmv_doubles_threshold: FloatProperty(
        name="Threshold", default=0.0001, precision=4, step=0.004, min=0)

    def execute(self, context):

        obj = bpy.context.active_object
        bm = bmesh.from_edit_mesh(obj.data)
        bm.normal_update()

        bm.verts.ensure_lookup_table()
        history = bm.select_history[:]

        sel = [v.index for v in bm.verts if v.select]
        sel_count = len(sel)

        if sel_count > 3:
            try:
                V0 = history[-1]
                V1 = history[-2]  # 2last verts
                if not isinstance(V0, bmesh.types.BMVert):
                    raise IndexError
                if not isinstance(V1, bmesh.types.BMVert):
                    raise IndexError

            except IndexError:
                self.report({'WARNING'}, "Need 2 last vertices")
                return {'CANCELLED'}

            v0id = V0.index  # after subdiv index will be needed because history changed
            v1id = V1.index

            vertlist = []  # all vert selected except 2 last one
            vertlistid = []

            bm.verts.ensure_lookup_table()

            vertlist = [v for v in bm.verts
                        if (v.select and v != V0 and v != V1)]
            vertlist = vertlist[:]

# find extrem in vertlist

            v_double_count = [v for v in vertlist
                              for e in v.link_edges if e.other_vert(v) in vertlist]
            extremcount = [(v.index, v)
                           for v in v_double_count if v_double_count.count(v) < 2]
            try:
                E0, E1 = extremcount[:]
            except ValueError:
                self.report({'WARNING'}, "Invalid selection")
                return {'CANCELLED'}


# connect V0V1 if not

            bmesh.ops.connect_verts(bm, verts=[V0, V1])

            try:
                bm.edges.new([V1, V0])
            except:
                pass

# delete faces to have no doubles after

            for v in vertlist:
                for f in V0.link_faces:
                    if V1 in f.verts and v in f.verts:
                        f.select = True
                        break

            faces = [f for f in bm.faces if f.select]
            bmesh.ops.delete(bm, geom=faces, context='FACES_ONLY')

# connect extrems and V0 V1 if not

            if (E0[1].co - V0.co).length <= (E0[1].co - V1.co).length:

                bmesh.ops.connect_verts(bm, verts=[E0[1], V0])
                try:
                    bm.edges.new([E0[1], V0])
                except:  # ValueError:
                    pass
                bmesh.ops.connect_verts(bm, verts=[E1[1], V1])
                try:
                    bm.edges.new([E1[1], V1])
                except:
                    pass
            else:
                bmesh.ops.connect_verts(bm, verts=[E0[1], V1])
                try:
                    bm.edges.new([E0[1], V1])
                except:
                    pass
                bmesh.ops.connect_verts(bm, verts=[E1[1], V0])
                try:
                    bm.edges.new([E1[1], V0])
                except:
                    pass

# subdiv and get new verts

            for v in bm.verts:
                v.select = False
            V0.select = True  # select edge v0v1
            V1.select = True
            bm.select_flush_mode()

            edges = [e for e in bm.edges if e.select]
            newmesh = bmesh.ops.subdivide_edges(
                bm, edges=edges, cuts=sel_count-4)

            newid = []  # get id new vertices

            bm.verts.ensure_lookup_table()

            for i in newmesh['geom_split']:
                if type(i) == bmesh.types.BMVert:
                    newid.append(i.index)

# Add faces

            bm.verts.ensure_lookup_table()
            allvertid = sel+newid

            for i in allvertid:
                bm.verts[i].select = True

            bm.select_flush_mode()

            V0 = bm.verts[v0id]
            V1 = bm.verts[v1id]

            bm.edges.ensure_lookup_table()

            v2 = None
            for e in V0.link_edges:  # deselect adjacent edges
                v2 = e.other_vert(V0)
                if v2.index == E0[0] or v2.index == E1[0]:
                    e.select = False
                    break

            v2 = None
            for e in V1.link_edges:
                v2 = e.other_vert(V1)
                if v2.index == E0[0] or v2.index == E1[0]:
                    e.select = False
                    break

            edges1 = [e for e in bm.edges if e.select]

            try:
                bmesh.ops.bridge_loops(bm, edges=edges1)  # bridge loops

            except RuntimeError:
                self.report({'WARNING'}, "Need 2 edges loops")
                return {'CANCELLED'}
# Next selection

            bpy.ops.mesh.select_all(action='DESELECT')

            V0.select = True
            V1.select = True
            for i in newid:
                bm.verts[i].select = True  # ok

            bm.verts.ensure_lookup_table()

            v2 = None
            othervertid = []
            othervert = []
            next = []
#            bm.verts.ensure_lookup_table()
            for e in V0.link_edges:
                v2 = e.other_vert(V0)

                if v2.index in vertlist:
                    continue
                if v2.index in newid:
                    continue
                if v2 == V1:
                    continue
                else:
                    next.append(v2)

            v3 = None
            for e in V1.link_edges:
                v3 = e.other_vert(V1)

                if v3.index in vertlist:
                    continue
                if v3.index in newid:
                    continue
                if v3 == V0:
                    continue
                else:
                    next.append(v3)

            for v in next:
                if v.index in newid:
                    continue
                if v.index in vertlist:
                    continue
                for f in v.link_faces:
                    if not V0 in f.verts:
                        continue
                    if not V1 in f.verts:
                        continue
                    else:
                        v.select = True
                        bm.select_history.add(v)

            bmesh.ops.remove_doubles(
                bm, verts=bm.verts, dist=self.rmv_doubles_threshold)
            bmesh.ops.dissolve_degenerate(bm, edges=bm.edges, dist=0.0001)
            bmesh.update_edit_mesh(obj.data)

            return {'FINISHED'}

        else:
            self.report({'WARNING'}, "select more vertices")
            return {'CANCELLED'}


class MULTI_OT_JOIN1(bpy.types.Operator):
    """Tooltip"""
    bl_idname = "multi.join"
    bl_label = "Multijoin"
    bl_options = {"UNDO"}

    def execute(self, context):  # "selected objects"

        obj = bpy.context.object
        me = obj.data
        bm = bmesh.from_edit_mesh(me)
        bm.normal_update()
        bm.verts.ensure_lookup_table()
        actvert = bm.select_history.active
        if (isinstance(actvert, bmesh.types.BMVert)):
            vertlist = []
            for v in bm.verts:
                if v.select:
                    if not(v == actvert):
                        vertlist.append(v)

            # do connection
            for v in vertlist:
                for f in actvert.link_faces:
                    if v in f.verts:
                        # when already face: split it
                        bmesh.ops.connect_verts(bm, verts=[v, actvert])

            for v in vertlist:
                v2 = None
                for e in v.link_edges:
                    v2 = e.other_vert(v)
                    if v2 in vertlist:
                        already = False
                        for f in actvert.link_faces:
                            if v in f.verts and v2 in f.verts:
                                already = True
                                break
                        # if no face already between first and selected vert: make it
                        if not(already):
                            bm.faces.new([v, actvert, v2])

            bmesh.ops.dissolve_degenerate(bm, edges=bm.edges, dist=0.0001)

            face_sel = [f for f in bm.faces if f.select]
            bmesh.ops.recalc_face_normals(bm, faces=face_sel)

            bmesh.update_edit_mesh(me)
            
            return {'FINISHED'}

        else:

            self.report({'WARNING'}, "No last selected vertex")

        return {'CANCELLED'}


class MULTIJOIN_MT_MENU (Menu):
    bl_label = ""

    def draw(self, context):
        layout = self.layout

        pie = layout.menu_pie()

        pie.operator("multi.join", text="Multi Join")
        pie.operator("join.slide", text="Slide and Join")
        pie.operator("mesh.vert_connect", text="Connect vert pairs")
        pie.operator("mesh.vert_connect_path", text="default join")

disabled_kmis = []

def get_active_kmi(space: str, **kwargs) -> bpy.types.KeyMapItem:
    kc = bpy.context.window_manager.keyconfigs.active
    km = kc.keymaps.get(space)
    if km:
        for kmi in km.keymap_items:
            for key, val in kwargs.items():
                if getattr(kmi, key) != val and val is not None:
                    break
            else:
                return kmi

def disable_shift_s_snap_kmi():
    # Be explicit with modifiers shift/ctrl/alt so we don't
    # accidentally disable a different keymap with same traits.
    kmi = get_active_kmi("Mesh",
                         idname="mesh.vert_connect_path",
                         type='J',
                         shift=False,
                         ctrl=False,
                         alt=False)
                         
    if kmi is not None:
        kmi.active = False
        disabled_kmis.append(kmi)


addon_keymaps = []

classes = (SLIDE_OT_JOIN, MULTI_OT_JOIN1, MULTIJOIN_MT_MENU, ADVANCED_OT_JOIN)


# A dictionary with keymap items to disable.
# The key is the space type.
# The value is the keymap item traits.
to_disable = {
    "Mesh": {"idname": "mesh.vert_connect_path",
             "type": 'J',
             "shift": False,
             "ctrl": False,
             "alt": False}
}


def register():

    keymaps = bpy.context.window_manager.keyconfigs.active.keymaps
    if not all(km in keymaps and get_active_kmi(km, **traits)
               for (km, traits) in to_disable.items()):

        # Keep track of how many times we retry. 20 retries == 2 seconds.
        register.retries = getattr(register, "retries", 0) + 1

        if register.retries < 20:
            return bpy.app.timers.register(register, first_interval=0.1)


    for cls in classes:
        bpy.utils.register_class(cls)

    disable_shift_s_snap_kmi()

    wm = bpy.context.window_manager
    kc = wm.keyconfigs.addon
    if kc:
        km = kc.keymaps.new(name='Mesh')
        kmi = km.keymap_items.new(idname='advanced.join', type='J', value='CLICK')
        addon_keymaps.append((km, kmi))
        kmi = km.keymap_items.new(idname='wm.call_menu_pie', type='J', value='CLICK_DRAG')
        kmi.properties.name = "MULTIJOIN_MT_MENU"
        addon_keymaps.append((km, kmi))


def unregister():

    for cls in classes:
        bpy.utils.unregister_class(cls)
        
    for kmi in disabled_kmis:
        kmi.active = True

    wm = bpy.context.window_manager
    kc = wm.keyconfigs.addon
    if kc is not None:
        for km, kmi in addon_keymaps:
            km.keymap_items.remove(kmi)
            
    addon_keymaps.clear()
1 Like

well amazing, now this is working. (I don’t need to put a condition if blender version < 3.1)
so it seems this is an improvement under 3.1. I guess for hotkeys this is better too

1 Like