1 # SPDX-License-Identifier: GPL-2.0-or-later
5 "name": "Edit Operator Source",
6 "author": "scorpion81",
9 "location": "Text Editor > Sidebar > Edit Operator",
10 "description": "Opens source file of chosen operator or call locations, if source not available",
12 "doc_url": "{BLENDER_MANUAL_URL}/addons/development/edit_operator.html",
13 "category": "Development",
20 from bpy
.types
import (
27 from bpy
.props
import (
33 def stdlib_excludes():
34 #need a handy list of modules to avoid walking into
35 import distutils
.sysconfig
as sysconfig
37 std_lib
= sysconfig
.get_python_lib(standard_lib
=True)
38 for top
, dirs
, files
in os
.walk(std_lib
):
40 if nm
!= '__init__.py' and nm
[-3:] == '.py':
41 excludes
.append(os
.path
.join(top
, nm
)[len(std_lib
)+1:-3].replace('\\','.'))
45 def make_loc(prefix
, c
):
46 #too long and not helpful... omitting for now
48 #if hasattr(c, "bl_space_type"):
49 # space = c.bl_space_type
52 #if hasattr(c, "bl_region_type"):
53 # region = c.bl_region_type
56 if hasattr(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"):
76 src
, n
= inspect
.getsourcelines(m
.draw
)
77 for i
, s
in enumerate(src
):
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
])
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
99 clazz
= getattr(bpy
.types
, id)
101 except AttributeError:
105 def getmodule(opname
):
107 clazz
= getclazz(opname
)
112 modn
= clazz
.__module
__
115 line
= inspect
.getsourcelines(clazz
)[1]
121 if modn
== 'bpy.types':
124 elif modn
!= '__main__':
125 mod
= sys
.modules
[modn
].__file
__
130 return mod
, line
, addon
135 opsdir
= dir(bpy
.ops
)
136 for opmodname
in opsdir
:
137 opmod
= getattr(bpy
.ops
, opmodname
)
138 opmoddir
= dir(opmod
)
140 name
= opmodname
+ "." + o
141 clazz
= getclazz(name
)
142 #if (clazz is not None) :# and clazz.__module__ != 'bpy.types'):
146 # add own operator name too, since its not loaded yet when this is called
147 allops
.append("text.edit_operator")
152 return [(y
, y
, "", x
) for x
, y
in enumerate(l
)]
154 class OperatorEntry(PropertyGroup
):
156 label
: StringProperty(
162 path
: StringProperty(
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"
188 path
: StringProperty(
200 def show_text(self
, context
, path
, line
):
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
209 bpy
.ops
.text
.jump(ctx
, line
=line
)
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
):
223 exclude
= stdlib_excludes()
224 exclude
.append("bpy")
225 exclude
.append("sys")
228 walk_module(self
.op
, bl_ui
, calls
, exclude
)
230 for m
in addon_utils
.modules():
232 mod
= sys
.modules
[m
.__name
__]
233 walk_module(self
.op
, mod
, calls
, exclude
)
238 cl
= context
.scene
.calls
.add()
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
)
254 context
.scene
.calls
.clear()
255 path
, line
, addon
= getmodule(self
.op
)
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()
267 self
.show_calls(context
)
268 context
.area
.tag_redraw()
273 self
.report({'WARNING'},
274 "Found no source file for " + self
.op
)
276 self
.show_calls(context
)
277 context
.area
.tag_redraw()
282 class TEXT_PT_EditOperatorPanel(Panel
):
283 bl_space_type
= 'TEXT_EDITOR'
284 bl_region_type
= 'UI'
285 bl_label
= "Edit Operator"
287 bl_options
= {'DEFAULT_CLOSED'}
289 def draw(self
, context
):
291 op
= layout
.operator("text.edit_operator")
295 if len(context
.scene
.calls
) > 0:
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
)
307 bpy
.utils
.register_class(OperatorEntry
)
308 bpy
.types
.Scene
.calls
= bpy
.props
.CollectionProperty(name
="Calls",
310 bpy
.utils
.register_class(TEXT_OT_EditOperator
)
311 bpy
.utils
.register_class(TEXT_PT_EditOperatorPanel
)
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__":