From 258b228ba610db1f0e64693c961f3f6c28661179 Mon Sep 17 00:00:00 2001 From: SpectralVectors Date: Wed, 10 Apr 2024 17:20:57 -0400 Subject: [PATCH] Keymap, Cursor Reset Fix, Distance Removed Fixed the keymap registration (again, and hopefully for the last time), I had been trying to make all the keymap changes on a single map, not realizing that I should have been switching between the addon keymap that I had created and the user's active keymap. Cursor Reset behavior seems to have been updated in Blender so that the cursor will remember its last screen position. This is the 'patched' behavior that this addon previously had implemented, which made the Cursor Reset setting redundant. I have implemented the previous 'cursor centering' behavior as the new 'Cursor Reset' behavior. The settings have flipped, but now the old behavior remains. The distance threshold did not have any effect on the addon, so it has been removed. If this is something that is missed, I can see about reimplementing it. --- Preferences.py | 42 +++++++++++----------- RightMouseNavigation.py | 72 ++++++++++++++++++++----------------- __init__.py | 80 +++++++++++++++++++---------------------- 3 files changed, 96 insertions(+), 98 deletions(-) diff --git a/Preferences.py b/Preferences.py index ad8e0fa..1476471 100644 --- a/Preferences.py +++ b/Preferences.py @@ -1,16 +1,23 @@ import bpy +from bpy.props import ( + BoolProperty, + FloatProperty, +) +from bpy.types import AddonPreferences def update_node_keymap(self, context): wm = context.window_manager - kc = wm.keyconfigs.user - for key in kc.keymaps['Node Editor'].keymap_items: + active_kc = wm.keyconfigs.active + for key in active_kc.keymaps['Node Editor'].keymap_items: if ( key.idname == "wm.call_menu" and key.type == "RIGHTMOUSE" ): key.active = not key.active + addon_kc = wm.keyconfigs.addon + for key in addon_kc.keymaps['Node Editor'].keymap_items: if ( key.idname == "blui.right_mouse_navigation" and key.type == "RIGHTMOUSE" @@ -18,32 +25,25 @@ def update_node_keymap(self, context): key.active = not key.active -class RightMouseNavigationPreferences(bpy.types.AddonPreferences): +class RightMouseNavigationPreferences(AddonPreferences): bl_idname = __package__ - time: bpy.props.FloatProperty( + time: FloatProperty( name="Time Threshold", description="How long you have hold right mouse to open menu", - default=0.3, + default=1.0, min=0.1, - max=2 + max=10, ) - distance: bpy.props.FloatProperty( - name="Distance Threshold", - description="How far you have to move the mouse to trigger navigation", - default=20, - min=1, - max=200 - ) - - reset_cursor_on_exit: bpy.props.BoolProperty( + reset_cursor_on_exit: BoolProperty( name="Reset Cursor on Exit", - description="After exiting navigation, this determines if the cursor resets to where RMB was clicked (if checked) or stays in the center (if unchecked)", - default=True + description="After exiting navigation, this determines if the cursor stays " + "where RMB was clicked (if unchecked) or resets to the center (if checked)", + default=False, ) - enable_for_node_editors: bpy.props.BoolProperty( + enable_for_node_editors: BoolProperty( name="Enable for Node Editors", description="Right Mouse will pan the view / open the Node Add/Search Menu", default=False, @@ -55,9 +55,7 @@ def draw(self, context): box = layout.box() box.label(text="Menu / Movement", icon='DRIVER_DISTANCE') - row = box.row() - row.prop(self, 'time') - row.prop(self, 'distance') + box.prop(self, 'time') row = layout.row() box = row.box() @@ -65,4 +63,4 @@ def draw(self, context): box.prop(self, 'reset_cursor_on_exit') box = row.box() box.label(text='Node Editor', icon='NODETREE') - box.prop(self, 'enable_for_node_editors') \ No newline at end of file + box.prop(self, 'enable_for_node_editors') diff --git a/RightMouseNavigation.py b/RightMouseNavigation.py index e4a7892..1884dea 100644 --- a/RightMouseNavigation.py +++ b/RightMouseNavigation.py @@ -1,6 +1,10 @@ -import bpy, ctypes, math, sys +import bpy +from bpy.types import Operator +import ctypes +import sys -class BLUI_OT_right_mouse_navigation(bpy.types.Operator): + +class BLUI_OT_right_mouse_navigation(Operator): """Timer that decides whether to display a menu after Right Click""" bl_idname = "blui.right_mouse_navigation" bl_label = "Right Mouse Navigation" @@ -8,7 +12,6 @@ class BLUI_OT_right_mouse_navigation(bpy.types.Operator): _timer = None _count = 0 - _move_distance = 0 MOUSE_RIGHTUP = 0x0010 _finished = False _callMenu = False @@ -27,34 +30,44 @@ class BLUI_OT_right_mouse_navigation(bpy.types.Operator): 'PAINT_VERTEX': 'VIEW3D_PT_paint_vertex_context_menu', 'PAINT_WEIGHT': 'VIEW3D_PT_paint_weight_context_menu', 'PAINT_TEXTURE': 'VIEW3D_PT_paint_texture_context_menu', - 'SCULPT': 'VIEW3D_PT_sculpt_context_menu'} + 'SCULPT': 'VIEW3D_PT_sculpt_context_menu', + } def modal(self, context, event): preferences = context.preferences addon_prefs = preferences.addons[__package__].preferences + enable_nodes = addon_prefs.enable_for_node_editors + + space_type = context.space_data.type - if context.space_data.type == 'VIEW_3D': + if space_type == 'VIEW_3D': # Check if the Viewport is Perspective or Orthographic if bpy.context.region_data.is_perspective: self._ortho = False else: self._back_to_ortho = True - # The _finished Boolean acts as a flag to exit the modal loop, + # The _finished Boolean acts as a flag to exit the modal loop, # it is not made True until after the cancel function is called if self._finished: def reset_cursor(): # Reset blender window cursor to previous position - context.window.cursor_warp(self.view_x, self.view_y) + area = context.area + x = area.x + y = area.y + x += int(area.width / 2) + y += int(area.height / 2) + bpy.context.window.cursor_warp(x, y) if self._callMenu: # Always reset the cursor if menu is called, as that implies a canceled navigation - reset_cursor() + if addon_prefs.reset_cursor_on_exit and not space_type == 'NODE_EDITOR': + reset_cursor() self.callMenu(context) else: - # Exit of a full navigation. Only reset the cursor if the preference (default False) is enabled + # Exit of a full navigation. Only reset the cursor if the preference is enabled if addon_prefs.reset_cursor_on_exit: reset_cursor() @@ -63,44 +76,37 @@ def reset_cursor(): return {'CANCELLED'} - if context.space_data.type == 'VIEW_3D' or addon_prefs.enable_for_node_editors and context.space_data.type == 'NODE_EDITOR': - # Calculate mousemove distance before it reaches threshold - if event.type == "MOUSEMOVE" and self._move_distance < addon_prefs.distance: - xDelta = event.mouse_x - event.mouse_prev_x - yDelta = event.mouse_y - event.mouse_prev_y - deltaDistance = math.sqrt(xDelta*xDelta + yDelta*yDelta) - self._move_distance += deltaDistance - + if space_type == 'VIEW_3D' or space_type == 'NODE_EDITOR' and enable_nodes: if event.type in {'RIGHTMOUSE'}: if event.value in {'RELEASE'}: - if sys.platform == 'win32': + if sys.platform.startswith('win'): # This fakes a Right Mouse Up event using Ctypes ctypes.windll.user32.mouse_event(self.MOUSE_RIGHTUP) # This brings back our mouse cursor to use with the menu context.window.cursor_modal_restore() - # If the length of time you've been holding down - # Right Mouse and Mouse move distance is longer than the threshold value, + # If the length of time you've been holding down + # Right Mouse and Mouse move distance is longer than the threshold value, # then set flag to call a context menu - if self._move_distance < addon_prefs.distance and self._count < addon_prefs.time: + if self._count < addon_prefs.time: self._callMenu = True self.cancel(context) - # We now set the flag to true to exit the modal operator on the next loop through + # We now set the flag to true to exit the modal operator on the next loop self._finished = True return {'PASS_THROUGH'} if event.type == 'TIMER': if self._count <= addon_prefs.time: - self._count += 0.01 + self._count += 0.1 return {'PASS_THROUGH'} def callMenu(self, context): if context.space_data.type == 'NODE_EDITOR': if context.space_data.node_tree: - bpy.ops.wm.search_menu('INVOKE_DEFAULT') + bpy.ops.wm.search_single_menu('INVOKE_DEFAULT', menu_idname='NODE_MT_add') else: try: bpy.ops.wm.call_menu(name=self.menu_by_mode[context.mode]) - except: + except RuntimeError: bpy.ops.wm.call_panel(name=self.menu_by_mode[context.mode]) def invoke(self, context, event): @@ -112,10 +118,13 @@ def invoke(self, context, event): def execute(self, context): preferences = context.preferences addon_prefs = preferences.addons[__package__].preferences + enable_nodes = addon_prefs.enable_for_node_editors + + space_type = context.space_data.type # Execute is the first thing called in our operator, so we start by # calling Blender's built-in Walk Navigation - if context.space_data.type == 'VIEW_3D': + if space_type == 'VIEW_3D': bpy.ops.view3d.walk('INVOKE_DEFAULT') # Adding the timer and starting the loop wm = context.window_manager @@ -123,19 +132,18 @@ def execute(self, context): wm.modal_handler_add(self) return {'RUNNING_MODAL'} - elif addon_prefs.enable_for_node_editors and context.space_data.type == 'NODE_EDITOR': + elif space_type == 'NODE_EDITOR' and enable_nodes: bpy.ops.view2d.pan('INVOKE_DEFAULT') - wm = context.window_manager # Adding the timer and starting the loop - self._timer = wm.event_timer_add(0.1, window=context.window) + self._timer = wm.event_timer_add(0.01, window=context.window) wm.modal_handler_add(self) return {'RUNNING_MODAL'} - - elif context.space_data.type == 'IMAGE_EDITOR': + + elif space_type == 'IMAGE_EDITOR': bpy.ops.wm.call_panel(name="VIEW3D_PT_paint_texture_context_menu") return {'FINISHED'} def cancel(self, context): wm = context.window_manager - wm.event_timer_remove(self._timer) \ No newline at end of file + wm.event_timer_remove(self._timer) diff --git a/__init__.py b/__init__.py index efbf8d0..27ad760 100644 --- a/__init__.py +++ b/__init__.py @@ -1,17 +1,16 @@ +from .Preferences import RightMouseNavigationPreferences +from .RightMouseNavigation import BLUI_OT_right_mouse_navigation +import bpy + bl_info = { 'name': 'Right Mouse Navigation', 'category': '3D View', 'author': 'Spectral Vectors', - 'version': (2, 1, 0), + 'version': (2, 2, 0), 'blender': (2, 90, 0), 'location': '3D Viewport, Node Editor', "description": "Enables Right Mouse Viewport Navigation" - } - -import bpy - -from .RightMouseNavigation import BLUI_OT_right_mouse_navigation -from .Preferences import RightMouseNavigationPreferences +} addon_keymaps = [] @@ -22,50 +21,41 @@ def register(): bpy.utils.register_class(RightMouseNavigationPreferences) bpy.utils.register_class(BLUI_OT_right_mouse_navigation) - register_keymaps() - - -def register_keymaps(): - keyconfig = bpy.context.window_manager.keyconfigs - areas = 'Window', 'Text', 'Object Mode', '3D View', 'Image', 'Node Editor' - - if not all(i in keyconfig.active.keymaps for i in areas): - bpy.app.timers.register(register_keymaps, first_interval=0.1) - - else: - wm = bpy.context.window_manager - kc = wm.keyconfigs.user + addon_kc = wm.keyconfigs.addon - km = kc.keymaps['3D View'] + km = addon_kc.keymaps.new(name='3D View', space_type='VIEW_3D') kmi = km.keymap_items.new( "blui.right_mouse_navigation", 'RIGHTMOUSE', 'PRESS' - ) + ) kmi.active = True - km2 = kc.keymaps['Node Editor'] + km2 = addon_kc.keymaps.new(name='Node Editor', space_type='NODE_EDITOR') kmi2 = km2.keymap_items.new( "blui.right_mouse_navigation", 'RIGHTMOUSE', 'PRESS' - ) + ) kmi2.active = False addon_keymaps.append((km, kmi, km2, kmi2)) - menumodes = ["Object Mode", "Mesh", "Curve", "Armature", "Metaball", "Lattice", "Font", "Pose"] + active_kc = wm.keyconfigs.active + + menumodes = ["Object Mode", "Mesh", "Curve", + "Armature", "Metaball", "Lattice", "Font", "Pose"] panelmodes = ["Vertex Paint", "Weight Paint", "Image Paint", "Sculpt"] # These Modes all call standard menus # "Object Mode", "Mesh", "Curve", "Armature", "Metaball", "Lattice", # "Font", "Pose" for i in menumodes: - for key in kc.keymaps[i].keymap_items: + for key in active_kc.keymaps[i].keymap_items: if ( - key.idname == "wm.call_menu" - and key.type == "RIGHTMOUSE" + # key.idname == "wm.call_menu" + key.type == "RIGHTMOUSE" and key.active ): key.active = False @@ -73,7 +63,7 @@ def register_keymaps(): # These Modes call panels instead of menus # "Vertex Paint", "Weight Paint", "Image Paint", "Sculpt" for i in panelmodes: - for key in kc.keymaps[i].keymap_items: + for key in active_kc.keymaps[i].keymap_items: if ( key.idname == "wm.call_panel" and key.type == "RIGHTMOUSE" @@ -81,15 +71,15 @@ def register_keymaps(): ): key.active = False - # Changing the Walk Modal Map - for key in kc.keymaps["View3D Walk Modal"].keymap_items: + # Changing the Walk Modal Map + for key in active_kc.keymaps["View3D Walk Modal"].keymap_items: if ( key.propvalue == "CANCEL" and key.type == "RIGHTMOUSE" and key.active ): key.active = False - for key in kc.keymaps["View3D Walk Modal"].keymap_items: + for key in active_kc.keymaps["View3D Walk Modal"].keymap_items: if ( key.propvalue == "CONFIRM" and key.type == "LEFTMOUSE" @@ -97,31 +87,32 @@ def register_keymaps(): ): key.type = "RIGHTMOUSE" key.value = "RELEASE" - pass def unregister(): if not bpy.app.background: + bpy.utils.unregister_class(BLUI_OT_right_mouse_navigation) bpy.utils.unregister_class(RightMouseNavigationPreferences) wm = bpy.context.window_manager - kc = wm.keyconfigs.user + active_kc = wm.keyconfigs.active - for key in kc.keymaps['Node Editor'].keymap_items: + for key in active_kc.keymaps['Node Editor'].keymap_items: if (key.idname == 'blui.right_mouse_navigation'): - kc.keymaps['Node Editor'].keymap_items.remove(key) + active_kc.keymaps['Node Editor'].keymap_items.remove(key) addon_keymaps.clear() - menumodes = ["Object Mode", "Mesh", "Curve", "Armature", "Metaball", "Lattice", "Font", "Pose", "Node Editor"] + menumodes = ["Object Mode", "Mesh", "Curve", "Armature", + "Metaball", "Lattice", "Font", "Pose", "Node Editor"] panelmodes = ["Vertex Paint", "Weight Paint", "Image Paint", "Sculpt"] # Reactivating menus # "Object Mode", "Mesh", "Curve", "Armature", "Metaball", "Lattice", # "Font", "Pose" for i in menumodes: - for key in kc.keymaps[i].keymap_items: + for key in active_kc.keymaps[i].keymap_items: if ( key.idname == "wm.call_menu" and key.type == "RIGHTMOUSE" @@ -129,23 +120,23 @@ def unregister(): key.active = True # Reactivating panels - # "Vertex Paint", "Weight Paint", "Image Paint", "Sculpt" + # "Vertex Paint", "Weight Paint", "Image Paint", "Sculpt" for i in panelmodes: - for key in kc.keymaps[i].keymap_items: + for key in active_kc.keymaps[i].keymap_items: if ( key.idname == "wm.call_panel" and key.type == "RIGHTMOUSE" ): key.active = True - # Changing the Walk Modal Map back - for key in kc.keymaps["View3D Walk Modal"].keymap_items: + # Changing the Walk Modal Map back + for key in active_kc.keymaps["View3D Walk Modal"].keymap_items: if ( key.propvalue == "CANCEL" and key.type == "RIGHTMOUSE" ): key.active = True - for key in kc.keymaps["View3D Walk Modal"].keymap_items: + for key in active_kc.keymaps["View3D Walk Modal"].keymap_items: if ( key.propvalue == "CONFIRM" and key.type == "RIGHTMOUSE" @@ -153,5 +144,6 @@ def unregister(): key.type = "LEFTMOUSE" key.value = "PRESS" + if __name__ == "__package__": - register() \ No newline at end of file + register()