Merge branch 'blender-v3.3-release'
[blender-addons.git] / development_edit_operator.py
blobbd406e49b5fc0c5462aa89af85f820bb15c7c197
1 # SPDX-License-Identifier: GPL-2.0-or-later
4 bl_info = {
5 "name": "Edit Operator Source",
6 "author": "scorpion81",
7 "version": (1, 2, 3),
8 "blender": (3, 2, 0),
9 "location": "Text Editor > Sidebar > Edit Operator",
10 "description": "Opens source file of chosen operator or call locations, if source not available",
11 "warning": "",
12 "doc_url": "{BLENDER_MANUAL_URL}/addons/development/edit_operator.html",
13 "category": "Development",
16 import bpy
17 import sys
18 import os
19 import inspect
20 from bpy.types import (
21 Operator,
22 Panel,
23 Header,
24 Menu,
25 PropertyGroup
27 from bpy.props import (
28 EnumProperty,
29 StringProperty,
30 IntProperty
33 def make_loc(prefix, c):
34 #too long and not helpful... omitting for now
35 space = ""
36 #if hasattr(c, "bl_space_type"):
37 # space = c.bl_space_type
39 region = ""
40 #if hasattr(c, "bl_region_type"):
41 # region = c.bl_region_type
43 label = ""
44 if hasattr(c, "bl_label"):
45 label = c.bl_label
47 return prefix+": " + space + " " + region + " " + label
49 def walk_module(opname, mod, calls=[], exclude=[]):
51 for name, m in inspect.getmembers(mod):
52 if inspect.ismodule(m):
53 if m.__name__ not in exclude:
54 #print(name, m.__name__)
55 walk_module(opname, m, calls, exclude)
56 elif inspect.isclass(m):
57 if (issubclass(m, Panel) or \
58 issubclass(m, Header) or \
59 issubclass(m, Menu)) and mod.__name__ != "bl_ui":
60 if hasattr(m, "draw"):
61 loc = ""
62 file = ""
63 line = -1
64 src, n = inspect.getsourcelines(m.draw)
65 for i, s in enumerate(src):
66 if opname in s:
67 file = mod.__file__
68 line = n + i
70 if issubclass(m, Panel) and name != "Panel":
71 loc = make_loc("Panel", m)
72 calls.append([opname, loc, file, line])
73 if issubclass(m, Header) and name != "Header":
74 loc = make_loc("Header", m)
75 calls.append([opname, loc, file, line])
76 if issubclass(m, Menu) and name != "Menu":
77 loc = make_loc("Menu", m)
78 calls.append([opname, loc, file, line])
81 def getclazz(opname):
82 opid = opname.split(".")
83 opmod = getattr(bpy.ops, opid[0])
84 op = getattr(opmod, opid[1])
85 id = op.get_rna_type().bl_rna.identifier
86 try:
87 clazz = getattr(bpy.types, id)
88 return clazz
89 except AttributeError:
90 return None
93 def getmodule(opname):
94 addon = True
95 clazz = getclazz(opname)
97 if clazz is None:
98 return "", -1, False
100 modn = clazz.__module__
102 try:
103 line = inspect.getsourcelines(clazz)[1]
104 except IOError:
105 line = -1
106 except TypeError:
107 line = -1
109 if modn == 'bpy.types':
110 mod = 'C operator'
111 addon = False
112 elif modn != '__main__':
113 mod = sys.modules[modn].__file__
114 else:
115 addon = False
116 mod = modn
118 return mod, line, addon
121 def get_ops():
122 allops = []
123 opsdir = dir(bpy.ops)
124 for opmodname in opsdir:
125 opmod = getattr(bpy.ops, opmodname)
126 opmoddir = dir(opmod)
127 for o in opmoddir:
128 name = opmodname + "." + o
129 clazz = getclazz(name)
130 #if (clazz is not None) :# and clazz.__module__ != 'bpy.types'):
131 allops.append(name)
132 del opmoddir
134 # add own operator name too, since its not loaded yet when this is called
135 allops.append("text.edit_operator")
136 l = sorted(allops)
137 del allops
138 del opsdir
140 return [(y, y, "", x) for x, y in enumerate(l)]
142 class OperatorEntry(PropertyGroup):
144 label : StringProperty(
145 name="Label",
146 description="",
147 default=""
150 path : StringProperty(
151 name="Path",
152 description="",
153 default=""
156 line : IntProperty(
157 name="Line",
158 description="",
159 default=-1
162 class TEXT_OT_EditOperator(Operator):
163 bl_idname = "text.edit_operator"
164 bl_label = "Edit Operator"
165 bl_description = "Opens the source file of operators chosen from Menu"
166 bl_property = "op"
168 items = get_ops()
170 op : EnumProperty(
171 name="Op",
172 description="",
173 items=items
176 path : StringProperty(
177 name="Path",
178 description="",
179 default=""
182 line : IntProperty(
183 name="Line",
184 description="",
185 default=-1
188 def show_text(self, context, path, line):
189 found = False
191 for t in bpy.data.texts:
192 if t.filepath == path:
193 #switch to the wanted text first
194 context.space_data.text = t
195 ctx = context.copy()
196 ctx['edit_text'] = t
197 bpy.ops.text.jump(ctx, line=line)
198 found = True
199 break
201 if (found is False):
202 self.report({'INFO'},
203 "Opened file: " + path)
204 bpy.ops.text.open(filepath=path)
205 bpy.ops.text.jump(line=line)
207 def show_calls(self, context):
208 import bl_ui
209 import addon_utils
210 import sys
212 exclude = []
213 exclude.extend(sys.stdlib_module_names)
214 exclude.append("bpy")
215 exclude.append("sys")
217 calls = []
218 walk_module(self.op, bl_ui, calls, exclude)
220 for m in addon_utils.modules():
221 try:
222 mod = sys.modules[m.__name__]
223 walk_module(self.op, mod, calls, exclude)
224 except KeyError:
225 continue
227 for c in calls:
228 cl = context.scene.calls.add()
229 cl.name = c[0]
230 cl.label = c[1]
231 cl.path = c[2]
232 cl.line = c[3]
234 def invoke(self, context, event):
235 context.window_manager.invoke_search_popup(self)
236 return {'PASS_THROUGH'}
238 def execute(self, context):
239 if self.path != "" and self.line != -1:
240 #invocation of one of the "found" locations
241 self.show_text(context, self.path, self.line)
242 return {'FINISHED'}
243 else:
244 context.scene.calls.clear()
245 path, line, addon = getmodule(self.op)
247 if addon:
248 self.show_text(context, path, line)
250 #add convenient "source" button, to toggle back from calls to source
251 c = context.scene.calls.add()
252 c.name = self.op
253 c.label = "Source"
254 c.path = path
255 c.line = line
257 self.show_calls(context)
258 context.area.tag_redraw()
260 return {'FINISHED'}
261 else:
263 self.report({'WARNING'},
264 "Found no source file for " + self.op)
266 self.show_calls(context)
267 context.area.tag_redraw()
269 return {'FINISHED'}
272 class TEXT_PT_EditOperatorPanel(Panel):
273 bl_space_type = 'TEXT_EDITOR'
274 bl_region_type = 'UI'
275 bl_label = "Edit Operator"
276 bl_category = "Text"
277 bl_options = {'DEFAULT_CLOSED'}
279 def draw(self, context):
280 layout = self.layout
281 op = layout.operator("text.edit_operator")
282 op.path = ""
283 op.line = -1
285 if len(context.scene.calls) > 0:
286 box = layout.box()
287 box.label(text="Calls of: " + context.scene.calls[0].name)
288 box.operator_context = 'EXEC_DEFAULT'
289 for c in context.scene.calls:
290 op = box.operator("text.edit_operator", text=c.label)
291 op.path = c.path
292 op.line = c.line
293 op.op = c.name
296 def register():
297 bpy.utils.register_class(OperatorEntry)
298 bpy.types.Scene.calls = bpy.props.CollectionProperty(name="Calls",
299 type=OperatorEntry)
300 bpy.utils.register_class(TEXT_OT_EditOperator)
301 bpy.utils.register_class(TEXT_PT_EditOperatorPanel)
304 def unregister():
305 bpy.utils.unregister_class(TEXT_PT_EditOperatorPanel)
306 bpy.utils.unregister_class(TEXT_OT_EditOperator)
307 del bpy.types.Scene.calls
308 bpy.utils.unregister_class(OperatorEntry)
311 if __name__ == "__main__":
312 register()