Cleanup: trailing space
[blender-addons.git] / development_edit_operator.py
blobe51df36eb0ad77db7ff7f4f4427328fbadea3f51
1 # SPDX-License-Identifier: GPL-2.0-or-later
4 bl_info = {
5 "name": "Edit Operator Source",
6 "author": "scorpion81",
7 "version": (1, 2, 2),
8 "blender": (2, 80, 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 stdlib_excludes():
34 #need a handy list of modules to avoid walking into
35 import distutils.sysconfig as sysconfig
36 excludes = []
37 std_lib = sysconfig.get_python_lib(standard_lib=True)
38 for top, dirs, files in os.walk(std_lib):
39 for nm in files:
40 if nm != '__init__.py' and nm[-3:] == '.py':
41 excludes.append(os.path.join(top, nm)[len(std_lib)+1:-3].replace('\\','.'))
43 return excludes
45 def make_loc(prefix, c):
46 #too long and not helpful... omitting for now
47 space = ""
48 #if hasattr(c, "bl_space_type"):
49 # space = c.bl_space_type
51 region = ""
52 #if hasattr(c, "bl_region_type"):
53 # region = c.bl_region_type
55 label = ""
56 if hasattr(c, "bl_label"):
57 label = c.bl_label
59 return prefix+": " + space + " " + region + " " + label
61 def walk_module(opname, mod, calls=[], exclude=[]):
63 for name, m in inspect.getmembers(mod):
64 if inspect.ismodule(m):
65 if m.__name__ not in exclude:
66 #print(name, m.__name__)
67 walk_module(opname, m, calls, exclude)
68 elif inspect.isclass(m):
69 if (issubclass(m, Panel) or \
70 issubclass(m, Header) or \
71 issubclass(m, Menu)) and mod.__name__ != "bl_ui":
72 if hasattr(m, "draw"):
73 loc = ""
74 file = ""
75 line = -1
76 src, n = inspect.getsourcelines(m.draw)
77 for i, s in enumerate(src):
78 if opname in s:
79 file = mod.__file__
80 line = n + i
82 if issubclass(m, Panel) and name != "Panel":
83 loc = make_loc("Panel", m)
84 calls.append([opname, loc, file, line])
85 if issubclass(m, Header) and name != "Header":
86 loc = make_loc("Header", m)
87 calls.append([opname, loc, file, line])
88 if issubclass(m, Menu) and name != "Menu":
89 loc = make_loc("Menu", m)
90 calls.append([opname, loc, file, line])
93 def getclazz(opname):
94 opid = opname.split(".")
95 opmod = getattr(bpy.ops, opid[0])
96 op = getattr(opmod, opid[1])
97 id = op.get_rna_type().bl_rna.identifier
98 try:
99 clazz = getattr(bpy.types, id)
100 return clazz
101 except AttributeError:
102 return None
105 def getmodule(opname):
106 addon = True
107 clazz = getclazz(opname)
109 if clazz is None:
110 return "", -1, False
112 modn = clazz.__module__
114 try:
115 line = inspect.getsourcelines(clazz)[1]
116 except IOError:
117 line = -1
118 except TypeError:
119 line = -1
121 if modn == 'bpy.types':
122 mod = 'C operator'
123 addon = False
124 elif modn != '__main__':
125 mod = sys.modules[modn].__file__
126 else:
127 addon = False
128 mod = modn
130 return mod, line, addon
133 def get_ops():
134 allops = []
135 opsdir = dir(bpy.ops)
136 for opmodname in opsdir:
137 opmod = getattr(bpy.ops, opmodname)
138 opmoddir = dir(opmod)
139 for o in opmoddir:
140 name = opmodname + "." + o
141 clazz = getclazz(name)
142 #if (clazz is not None) :# and clazz.__module__ != 'bpy.types'):
143 allops.append(name)
144 del opmoddir
146 # add own operator name too, since its not loaded yet when this is called
147 allops.append("text.edit_operator")
148 l = sorted(allops)
149 del allops
150 del opsdir
152 return [(y, y, "", x) for x, y in enumerate(l)]
154 class OperatorEntry(PropertyGroup):
156 label : StringProperty(
157 name="Label",
158 description="",
159 default=""
162 path : StringProperty(
163 name="Path",
164 description="",
165 default=""
168 line : IntProperty(
169 name="Line",
170 description="",
171 default=-1
174 class TEXT_OT_EditOperator(Operator):
175 bl_idname = "text.edit_operator"
176 bl_label = "Edit Operator"
177 bl_description = "Opens the source file of operators chosen from Menu"
178 bl_property = "op"
180 items = get_ops()
182 op : EnumProperty(
183 name="Op",
184 description="",
185 items=items
188 path : StringProperty(
189 name="Path",
190 description="",
191 default=""
194 line : IntProperty(
195 name="Line",
196 description="",
197 default=-1
200 def show_text(self, context, path, line):
201 found = False
203 for t in bpy.data.texts:
204 if t.filepath == path:
205 #switch to the wanted text first
206 context.space_data.text = t
207 ctx = context.copy()
208 ctx['edit_text'] = t
209 bpy.ops.text.jump(ctx, line=line)
210 found = True
211 break
213 if (found is False):
214 self.report({'INFO'},
215 "Opened file: " + path)
216 bpy.ops.text.open(filepath=path)
217 bpy.ops.text.jump(line=line)
219 def show_calls(self, context):
220 import bl_ui
221 import addon_utils
223 exclude = stdlib_excludes()
224 exclude.append("bpy")
225 exclude.append("sys")
227 calls = []
228 walk_module(self.op, bl_ui, calls, exclude)
230 for m in addon_utils.modules():
231 try:
232 mod = sys.modules[m.__name__]
233 walk_module(self.op, mod, calls, exclude)
234 except KeyError:
235 continue
237 for c in calls:
238 cl = context.scene.calls.add()
239 cl.name = c[0]
240 cl.label = c[1]
241 cl.path = c[2]
242 cl.line = c[3]
244 def invoke(self, context, event):
245 context.window_manager.invoke_search_popup(self)
246 return {'PASS_THROUGH'}
248 def execute(self, context):
249 if self.path != "" and self.line != -1:
250 #invocation of one of the "found" locations
251 self.show_text(context, self.path, self.line)
252 return {'FINISHED'}
253 else:
254 context.scene.calls.clear()
255 path, line, addon = getmodule(self.op)
257 if addon:
258 self.show_text(context, path, line)
260 #add convenient "source" button, to toggle back from calls to source
261 c = context.scene.calls.add()
262 c.name = self.op
263 c.label = "Source"
264 c.path = path
265 c.line = line
267 self.show_calls(context)
268 context.area.tag_redraw()
270 return {'FINISHED'}
271 else:
273 self.report({'WARNING'},
274 "Found no source file for " + self.op)
276 self.show_calls(context)
277 context.area.tag_redraw()
279 return {'FINISHED'}
282 class TEXT_PT_EditOperatorPanel(Panel):
283 bl_space_type = 'TEXT_EDITOR'
284 bl_region_type = 'UI'
285 bl_label = "Edit Operator"
286 bl_category = "Text"
287 bl_options = {'DEFAULT_CLOSED'}
289 def draw(self, context):
290 layout = self.layout
291 op = layout.operator("text.edit_operator")
292 op.path = ""
293 op.line = -1
295 if len(context.scene.calls) > 0:
296 box = layout.box()
297 box.label(text="Calls of: " + context.scene.calls[0].name)
298 box.operator_context = 'EXEC_DEFAULT'
299 for c in context.scene.calls:
300 op = box.operator("text.edit_operator", text=c.label)
301 op.path = c.path
302 op.line = c.line
303 op.op = c.name
306 def register():
307 bpy.utils.register_class(OperatorEntry)
308 bpy.types.Scene.calls = bpy.props.CollectionProperty(name="Calls",
309 type=OperatorEntry)
310 bpy.utils.register_class(TEXT_OT_EditOperator)
311 bpy.utils.register_class(TEXT_PT_EditOperatorPanel)
314 def unregister():
315 bpy.utils.unregister_class(TEXT_PT_EditOperatorPanel)
316 bpy.utils.unregister_class(TEXT_OT_EditOperator)
317 del bpy.types.Scene.calls
318 bpy.utils.unregister_class(OperatorEntry)
321 if __name__ == "__main__":
322 register()