Skip to content

Commit bf4a253

Browse files
Cross Platform Support, Big Refactor, Node Editors
I removed as much of the Ctypes code that relied upon the windll as I could, replacing it with Blender native functions. The only place it couldn't be removed was from the fake Right Mouse Release, which is only required on Windows anyway, so it worked itself out. (Still not sure why Windows needs it but Linux does not). Haven't tested on Mac yet, but theoretically it should work. (famous last words) Moved all the menu logic into a single dict that maps the context mode to the menu that the mode should call. This means that there is no string formatting happening when attempting to call a menu, which gave inconsistent performance and added needless overhead. You can now enable it for use in Node Editors - you can pan the view and open the Add/Search menu with RIght Mouse. Also cleaned up the Preferences panel, as well as renaming variables, removing whitespace and other small changes.
1 parent 8b1c096 commit bf4a253

3 files changed

Lines changed: 121 additions & 74 deletions

File tree

Preferences.py

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,35 @@
11
import bpy
22

3+
4+
def update_node_keymap(self, context):
5+
wm = context.window_manager
6+
kc = wm.keyconfigs.user
7+
for key in kc.keymaps['Node Editor'].keymap_items:
8+
if (
9+
key.idname == "wm.call_menu"
10+
and key.type == "RIGHTMOUSE"
11+
):
12+
key.active = not key.active
13+
14+
if (
15+
key.idname == "blui.right_mouse_navigation"
16+
and key.type == "RIGHTMOUSE"
17+
):
18+
key.active = not key.active
19+
20+
321
class RightMouseNavigationPreferences(bpy.types.AddonPreferences):
422
bl_idname = __package__
523

6-
timepreference: bpy.props.FloatProperty(
24+
time: bpy.props.FloatProperty(
725
name="Time Threshold",
826
description="How long you have hold right mouse to open menu",
927
default=0.3,
1028
min=0.1,
1129
max=2
1230
)
1331

14-
distancepreference: bpy.props.FloatProperty(
32+
distance: bpy.props.FloatProperty(
1533
name="Distance Threshold",
1634
description="How far you have to move the mouse to trigger navigation",
1735
default=20,
@@ -25,12 +43,26 @@ class RightMouseNavigationPreferences(bpy.types.AddonPreferences):
2543
default=True
2644
)
2745

46+
enable_for_node_editors: bpy.props.BoolProperty(
47+
name="Enable for Node Editors",
48+
description="Right Mouse will pan the view / open the Node Add/Search Menu",
49+
default=False,
50+
update=update_node_keymap,
51+
)
52+
2853
def draw(self, context):
2954
layout = self.layout
3055

31-
layout.label(text="Menu Trigger Preferences")
32-
layout.prop(self, 'timepreference')
33-
layout.prop(self, 'distancepreference')
56+
box = layout.box()
57+
box.label(text="Menu / Movement", icon='DRIVER_DISTANCE')
58+
row = box.row()
59+
row.prop(self, 'time')
60+
row.prop(self, 'distance')
3461

35-
layout.label(text="Cursor Preferences")
36-
layout.prop(self, 'reset_cursor_on_exit')
62+
row = layout.row()
63+
box = row.box()
64+
box.label(text="Cursor", icon='ORIENTATION_CURSOR')
65+
box.prop(self, 'reset_cursor_on_exit')
66+
box = row.box()
67+
box.label(text='Node Editor', icon='NODETREE')
68+
box.prop(self, 'enable_for_node_editors')

RightMouseNavigation.py

Lines changed: 53 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,4 @@
1-
import bpy, ctypes, math
2-
3-
class POINT(ctypes.Structure):
4-
_fields_ = [("x", ctypes.c_long), ("y", ctypes.c_long)]
1+
import bpy, ctypes, math, sys
52

63
class BLUI_OT_right_mouse_navigation(bpy.types.Operator):
74
"""Timer that decides whether to display a menu after Right Click"""
@@ -13,19 +10,36 @@ class BLUI_OT_right_mouse_navigation(bpy.types.Operator):
1310
_count = 0
1411
_move_distance = 0
1512
MOUSE_RIGHTUP = 0x0010
16-
MOUSE_MOVE = 0x0001
1713
_finished = False
1814
_callMenu = False
1915
_ortho = False
16+
_back_to_ortho = False
17+
menu_by_mode = {
18+
'OBJECT': 'VIEW3D_MT_object_context_menu',
19+
'EDIT_MESH': 'VIEW3D_MT_edit_mesh_context_menu',
20+
'EDIT_SURFACE': 'VIEW3D_MT_edit_surface',
21+
'EDIT_TEXT': 'VIEW3D_MT_edit_font_context_menu',
22+
'EDIT_ARMATURE': 'VIEW3D_MT_edit_armature',
23+
'EDIT_CURVE': 'VIEW3D_MT_edit_curve_context_menu',
24+
'EDIT_METABALL': 'VIEW3D_MT_edit_metaball_context_menu',
25+
'EDIT_LATTICE': 'VIEW3D_MT_edit_lattice_context_menu',
26+
'POSE': 'VIEW3D_MT_pose_context_menu',
27+
'PAINT_VERTEX': 'VIEW3D_PT_paint_vertex_context_menu',
28+
'PAINT_WEIGHT': 'VIEW3D_PT_paint_weight_context_menu',
29+
'PAINT_TEXTURE': 'VIEW3D_PT_paint_texture_context_menu',
30+
'SCULPT': 'VIEW3D_PT_sculpt_context_menu'}
2031

2132
def modal(self, context, event):
22-
33+
2334
preferences = context.preferences
2435
addon_prefs = preferences.addons[__package__].preferences
2536

26-
# Check if the Viewport is Perspective or Orthographic
27-
if bpy.context.region_data.is_perspective:
28-
self._ortho = False
37+
if context.space_data.type == 'VIEW_3D':
38+
# Check if the Viewport is Perspective or Orthographic
39+
if bpy.context.region_data.is_perspective:
40+
self._ortho = False
41+
else:
42+
self._back_to_ortho = True
2943

3044
# The _finished Boolean acts as a flag to exit the modal loop,
3145
# it is not made True until after the cancel function is called
@@ -34,8 +48,6 @@ def modal(self, context, event):
3448
def reset_cursor():
3549
# Reset blender window cursor to previous position
3650
context.window.cursor_warp(self.view_x, self.view_y)
37-
# Reset Windows OS cursor to previous position
38-
ctypes.windll.user32.SetCursorPos(self.mouse_x, self.mouse_y)
3951

4052
if self._callMenu:
4153
# Always reset the cursor if menu is called, as that implies a canceled navigation
@@ -45,98 +57,83 @@ def reset_cursor():
4557
# Exit of a full navigation. Only reset the cursor if the preference (default False) is enabled
4658
if addon_prefs.reset_cursor_on_exit:
4759
reset_cursor()
48-
60+
61+
if self._back_to_ortho:
62+
bpy.ops.view3d.view_persportho()
63+
4964
return {'CANCELLED'}
50-
51-
if context.space_data.type == 'VIEW_3D':
65+
66+
if context.space_data.type == 'VIEW_3D' or addon_prefs.enable_for_node_editors and context.space_data.type == 'NODE_EDITOR':
5267
# Calculate mousemove distance before it reaches threshold
53-
if event.type == "MOUSEMOVE" and self._move_distance < addon_prefs.distancepreference:
68+
if event.type == "MOUSEMOVE" and self._move_distance < addon_prefs.distance:
5469
xDelta = event.mouse_x - event.mouse_prev_x
5570
yDelta = event.mouse_y - event.mouse_prev_y
5671
deltaDistance = math.sqrt(xDelta*xDelta + yDelta*yDelta)
5772
self._move_distance += deltaDistance
5873

5974
if event.type in {'RIGHTMOUSE'}:
6075
if event.value in {'RELEASE'}:
61-
# This fakes a Right Mouse Up event using Ctypes
62-
ctypes.windll.user32.mouse_event(self.MOUSE_RIGHTUP)
76+
if sys.platform == 'win32':
77+
# This fakes a Right Mouse Up event using Ctypes
78+
ctypes.windll.user32.mouse_event(self.MOUSE_RIGHTUP)
6379
# This brings back our mouse cursor to use with the menu
6480
context.window.cursor_modal_restore()
6581
# If the length of time you've been holding down
6682
# Right Mouse and Mouse move distance is longer than the threshold value,
6783
# then set flag to call a context menu
68-
if self._move_distance < addon_prefs.distancepreference and self._count < addon_prefs.timepreference:
69-
# context.window.cursor_warp(self.view_x, self.view_y)
84+
if self._move_distance < addon_prefs.distance and self._count < addon_prefs.time:
7085
self._callMenu = True
7186
self.cancel(context)
7287
# We now set the flag to true to exit the modal operator on the next loop through
7388
self._finished = True
7489
return {'PASS_THROUGH'}
7590

7691
if event.type == 'TIMER':
77-
if self._count <= addon_prefs.timepreference:
92+
if self._count <= addon_prefs.time:
7893
self._count += 0.01
7994
return {'PASS_THROUGH'}
8095

8196
def callMenu(self, context):
82-
# try to call a context menu, and if that fails, call a context panel.
83-
# Most of Blender's context menus can be called with the code in the
84-
# 'try: except:' section, but there are a few modes that don't follow
85-
# the same conventions, these are accounted for here
86-
if context.mode == 'EDIT_ARMATURE':
87-
bpy.ops.wm.call_menu(
88-
name="VIEW3D_MT_" + context.mode.lower()[5:].strip() + "_context_menu"
89-
)
90-
return{'PASS_THROUGH'}
91-
if context.mode == 'EDIT_SURFACE':
92-
bpy.ops.wm.call_menu(
93-
name="VIEW3D_MT_" + context.mode.lower()
94-
)
95-
return{'PASS_THROUGH'}
96-
if context.mode == 'EDIT_TEXT':
97-
bpy.ops.wm.call_menu(
98-
name="VIEW3D_MT_edit_font_context_menu"
99-
)
100-
return{'PASS_THROUGH'}
97+
if context.space_data.type == 'NODE_EDITOR':
98+
if context.space_data.node_tree:
99+
bpy.ops.node.add_search('INVOKE_DEFAULT')
101100
else:
102101
try:
103-
bpy.ops.wm.call_menu(
104-
name="VIEW3D_MT_" + context.mode.lower() + "_context_menu"
105-
)
102+
bpy.ops.wm.call_menu(name=self.menu_by_mode[context.mode])
106103
except:
107-
bpy.ops.wm.call_panel(
108-
name="VIEW3D_PT_" + context.mode.lower() + "_context_menu"
109-
)
104+
bpy.ops.wm.call_panel(name=self.menu_by_mode[context.mode])
110105

111106
def invoke(self, context, event):
112-
# Store Windows OS cursor position
113-
cursor = POINT()
114-
ctypes.windll.user32.GetCursorPos(ctypes.byref(cursor))
115-
self.mouse_x = cursor.x
116-
self.mouse_y = cursor.y
117107
# Store Blender cursor position
118108
self.view_x = event.mouse_x
119109
self.view_y = event.mouse_y
120110
return self.execute(context)
121111

122112
def execute(self, context):
113+
preferences = context.preferences
114+
addon_prefs = preferences.addons[__package__].preferences
115+
123116
# Execute is the first thing called in our operator, so we start by
124117
# calling Blender's built-in Walk Navigation
125118
if context.space_data.type == 'VIEW_3D':
126119
bpy.ops.view3d.walk('INVOKE_DEFAULT')
127-
128-
if self._ortho:
129-
bpy.ops.view3d.view_persportho()
120+
# Adding the timer and starting the loop
121+
wm = context.window_manager
122+
self._timer = wm.event_timer_add(0.1, window=context.window)
123+
wm.modal_handler_add(self)
124+
return {'RUNNING_MODAL'}
125+
126+
elif addon_prefs.enable_for_node_editors and context.space_data.type == 'NODE_EDITOR':
127+
bpy.ops.view2d.pan('INVOKE_DEFAULT')
130128

131129
wm = context.window_manager
132130
# Adding the timer and starting the loop
133131
self._timer = wm.event_timer_add(0.1, window=context.window)
134132
wm.modal_handler_add(self)
135133
return {'RUNNING_MODAL'}
134+
136135
elif context.space_data.type == 'IMAGE_EDITOR':
137-
bpy.ops.wm.call_panel(
138-
name="VIEW3D_PT_" + context.mode.lower() + "_context_menu"
139-
)
136+
bpy.ops.wm.call_panel(name="VIEW3D_PT_paint_texture_context_menu")
140137
return {'FINISHED'}
141138

142139
def cancel(self, context):

__init__.py

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
bl_info = {
22
'name': 'Right Mouse Navigation',
3-
'category': 'View 3D',
3+
'category': '3D View',
44
'author': 'Spectral Vectors',
5-
'version': (0, 1, 9),
5+
'version': (2, 0, 0),
66
'blender': (2, 90, 0),
7-
'location': '3D Viewport',
7+
'location': '3D Viewport, Node Editor',
88
"description": "Enables Right Mouse Viewport Navigation"
99
}
1010

@@ -24,18 +24,19 @@ def register():
2424

2525
register_keymaps()
2626

27+
2728
def register_keymaps():
2829
keyconfig = bpy.context.window_manager.keyconfigs
29-
areas = 'Window', 'Text', 'Object Mode', '3D View'
30+
areas = 'Window', 'Text', 'Object Mode', '3D View', 'Image', 'Node Editor'
3031

3132
if not all(i in keyconfig.active.keymaps for i in areas):
3233
bpy.app.timers.register(register_keymaps, first_interval=0.1)
3334

3435
else:
35-
36+
3637
wm = bpy.context.window_manager
3738
kc = wm.keyconfigs.user
38-
39+
3940
km = kc.keymaps.new(
4041
name="3D View",
4142
space_type='VIEW_3D',
@@ -45,13 +46,26 @@ def register_keymaps():
4546
"blui.right_mouse_navigation",
4647
'RIGHTMOUSE',
4748
'PRESS'
48-
)
49+
)
4950
kmi.active = True
50-
addon_keymaps.append((km, kmi))
51+
52+
km2 = kc.keymaps.new(
53+
name="Node Editor",
54+
space_type='NODE_EDITOR',
55+
region_type='WINDOW'
56+
)
57+
kmi2 = km2.keymap_items.new(
58+
"blui.right_mouse_navigation",
59+
'RIGHTMOUSE',
60+
'PRESS'
61+
)
62+
kmi2.active = False
63+
64+
addon_keymaps.append((km, kmi, km2, kmi2))
5165

5266
menumodes = ["Object Mode", "Mesh", "Curve", "Armature", "Metaball", "Lattice", "Font", "Pose"]
5367
panelmodes = ["Vertex Paint", "Weight Paint", "Image Paint", "Sculpt"]
54-
68+
5569
# These Modes all call standard menus
5670
# "Object Mode", "Mesh", "Curve", "Armature", "Metaball", "Lattice",
5771
# "Font", "Pose"
@@ -102,11 +116,15 @@ def unregister():
102116
wm = bpy.context.window_manager
103117
kc = wm.keyconfigs.user
104118

119+
for key in kc.keymaps['Node Editor'].keymap_items:
120+
if (key.idname == 'blui.right_mouse_navigation'):
121+
kc.keymaps['Node Editor'].keymap_items.remove(key)
122+
105123
addon_keymaps.clear()
106124

107-
menumodes = ["Object Mode", "Mesh", "Curve", "Armature", "Metaball", "Lattice", "Font", "Pose"]
125+
menumodes = ["Object Mode", "Mesh", "Curve", "Armature", "Metaball", "Lattice", "Font", "Pose", "Node Editor"]
108126
panelmodes = ["Vertex Paint", "Weight Paint", "Image Paint", "Sculpt"]
109-
127+
110128
# Reactivating menus
111129
# "Object Mode", "Mesh", "Curve", "Armature", "Metaball", "Lattice",
112130
# "Font", "Pose"

0 commit comments

Comments
 (0)