Fix #104941: Node Wrangler cannot use both bump and normal
[blender-addons.git] / development_edit_operator.py
blob0eeeeec158454f3e429f7cc7978a8d689cdfba9e
1 # SPDX-FileCopyrightText: 2017-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
6 bl_info = {
7 "name": "Edit Operator Source",
8 "author": "scorpion81",
9 "version": (1, 2, 3),
10 "blender": (3, 2, 0),
11 "location": "Text Editor > Sidebar > Edit Operator",
12 "description": "Opens source file of chosen operator or call locations, if source not available",
13 "warning": "",
14 "doc_url": "{BLENDER_MANUAL_URL}/addons/development/edit_operator.html",
15 "category": "Development",
18 import bpy
19 import sys
20 import os
21 import inspect
22 from bpy.types import (
23 Operator,
24 Panel,
25 Header,
26 Menu,
27 PropertyGroup
29 from bpy.props import (
30 EnumProperty,
31 StringProperty,
32 IntProperty
35 def make_loc(prefix, c):
36 #too long and not helpful... omitting for now
37 space = ""
38 #if hasattr(c, "bl_space_type"):
39 # space = c.bl_space_type
41 region = ""
42 #if hasattr(c, "bl_region_type"):
43 # region = c.bl_region_type
45 label = ""
46 if hasattr(c, "bl_label"):
47 label = c.bl_label
49 return prefix+": " + space + " " + region + " " + label
51 def walk_module(opname, mod, calls=[], exclude=[]):
53 for name, m in inspect.getmembers(mod):
54 if inspect.ismodule(m):
55 if m.__name__ not in exclude:
56 #print(name, m.__name__)
57 walk_module(opname, m, calls, exclude)
58 elif inspect.isclass(m):
59 if (issubclass(m, Panel) or \
60 issubclass(m, Header) or \
61 issubclass(m, Menu)) and mod.__name__ != "bl_ui":
62 if hasattr(m, "draw"):
63 loc = ""
64 file = ""
65 line = -1
66 src, n = inspect.getsourcelines(m.draw)
67 for i, s in enumerate(src):
68 if opname in s:
69 file = mod.__file__
70 line = n + i
72 if issubclass(m, Panel) and name != "Panel":
73 loc = make_loc("Panel", m)
74 calls.append([opname, loc, file, line])
75 if issubclass(m, Header) and name != "Header":
76 loc = make_loc("Header", m)
77 calls.append([opname, loc, file, line])
78 if issubclass(m, Menu) and name != "Menu":
79 loc = make_loc("Menu", m)
80 calls.append([opname, loc, file, line])
83 def getclazz(opname):
84 opid = opname.split(".")
85 opmod = getattr(bpy.ops, opid[0])
86 op = getattr(opmod, opid[1])
87 id = op.get_rna_type().bl_rna.identifier
88 try:
89 clazz = getattr(bpy.types, id)
90 return clazz
91 except AttributeError:
92 return None
95 def getmodule(opname):
96 addon = True
97 clazz = getclazz(opname)
99 if clazz is None:
100 return "", -1, False
102 modn = clazz.__module__
104 try:
105 line = inspect.getsourcelines(clazz)[1]
106 except IOError:
107 line = -1
108 except TypeError:
109 line = -1
111 if modn == 'bpy.types':
112 mod = 'C operator'
113 addon = False
114 elif modn != '__main__':
115 mod = sys.modules[modn].__file__
116 else:
117 addon = False
118 mod = modn
120 return mod, line, addon
123 def get_ops():
124 allops = []
125 opsdir = dir(bpy.ops)
126 for opmodname in opsdir:
127 opmod = getattr(bpy.ops, opmodname)
128 opmoddir = dir(opmod)
129 for o in opmoddir:
130 name = opmodname + "." + o
131 clazz = getclazz(name)
132 #if (clazz is not None) :# and clazz.__module__ != 'bpy.types'):
133 allops.append(name)
134 del opmoddir
136 # add own operator name too, since its not loaded yet when this is called
137 allops.append("text.edit_operator")
138 l = sorted(allops)
139 del allops
140 del opsdir
142 return [(y, y, "", x) for x, y in enumerate(l)]
144 class OperatorEntry(PropertyGroup):
146 label : StringProperty(
147 name="Label",
148 description="",
149 default=""
152 path : StringProperty(
153 name="Path",
154 description="",
155 default=""
158 line : IntProperty(
159 name="Line",
160 description="",
161 default=-1
164 class TEXT_OT_EditOperator(Operator):
165 bl_idname = "text.edit_operator"
166 bl_label = "Edit Operator"
167 bl_description = "Opens the source file of operators chosen from Menu"
168 bl_property = "op"
170 items = get_ops()
172 op : EnumProperty(
173 name="Op",
174 description="",
175 items=items
178 path : StringProperty(
179 name="Path",
180 description="",
181 default=""
184 line : IntProperty(
185 name="Line",
186 description="",
187 default=-1
190 def show_text(self, context, path, line):
191 found = False
193 for t in bpy.data.texts:
194 if t.filepath == path:
195 #switch to the wanted text first
196 context.space_data.text = t
197 ctx = context.copy()
198 ctx['edit_text'] = t
199 bpy.ops.text.jump(ctx, line=line)
200 found = True
201 break
203 if (found is False):
204 self.report({'INFO'},
205 "Opened file: " + path)
206 bpy.ops.text.open(filepath=path)
207 bpy.ops.text.jump(line=line)
209 def show_calls(self, context):
210 import bl_ui
211 import addon_utils
212 import sys
214 exclude = []
215 exclude.extend(sys.stdlib_module_names)
216 exclude.append("bpy")
217 exclude.append("sys")
219 calls = []
220 walk_module(self.op, bl_ui, calls, exclude)
222 for m in addon_utils.modules():
223 try:
224 mod = sys.modules[m.__name__]
225 walk_module(self.op, mod, calls, exclude)
226 except KeyError:
227 continue
229 for c in calls:
230 cl = context.scene.calls.add()
231 cl.name = c[0]
232 cl.label = c[1]
233 cl.path = c[2]
234 cl.line = c[3]
236 def invoke(self, context, event):
237 context.window_manager.invoke_search_popup(self)
238 return {'PASS_THROUGH'}
240 def execute(self, context):
241 if self.path != "" and self.line != -1:
242 #invocation of one of the "found" locations
243 self.show_text(context, self.path, self.line)
244 return {'FINISHED'}
245 else:
246 context.scene.calls.clear()
247 path, line, addon = getmodule(self.op)
249 if addon:
250 self.show_text(context, path, line)
252 #add convenient "source" button, to toggle back from calls to source
253 c = context.scene.calls.add()
254 c.name = self.op
255 c.label = "Source"
256 c.path = path
257 c.line = line
259 self.show_calls(context)
260 context.area.tag_redraw()
262 return {'FINISHED'}
263 else:
265 self.report({'WARNING'},
266 "Found no source file for " + self.op)
268 self.show_calls(context)
269 context.area.tag_redraw()
271 return {'FINISHED'}
274 class TEXT_PT_EditOperatorPanel(Panel):
275 bl_space_type = 'TEXT_EDITOR'
276 bl_region_type = 'UI'
277 bl_label = "Edit Operator"
278 bl_category = "Text"
279 bl_options = {'DEFAULT_CLOSED'}
281 def draw(self, context):
282 layout = self.layout
283 op = layout.operator("text.edit_operator")
284 op.path = ""
285 op.line = -1
287 if len(context.scene.calls) > 0:
288 box = layout.box()
289 box.label(text="Calls of: " + context.scene.calls[0].name)
290 box.operator_context = 'EXEC_DEFAULT'
291 for c in context.scene.calls:
292 op = box.operator("text.edit_operator", text=c.label)
293 op.path = c.path
294 op.line = c.line
295 op.op = c.name
298 def register():
299 bpy.utils.register_class(OperatorEntry)
300 bpy.types.Scene.calls = bpy.props.CollectionProperty(name="Calls",
301 type=OperatorEntry)
302 bpy.utils.register_class(TEXT_OT_EditOperator)
303 bpy.utils.register_class(TEXT_PT_EditOperatorPanel)
306 def unregister():
307 bpy.utils.unregister_class(TEXT_PT_EditOperatorPanel)
308 bpy.utils.unregister_class(TEXT_OT_EditOperator)
309 del bpy.types.Scene.calls
310 bpy.utils.unregister_class(OperatorEntry)
313 if __name__ == "__main__":
314 register()