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 make_loc(prefix
, c
):
34 #too long and not helpful... omitting for now
36 #if hasattr(c, "bl_space_type"):
37 # space = c.bl_space_type
40 #if hasattr(c, "bl_region_type"):
41 # region = c.bl_region_type
44 if hasattr(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"):
64 src
, n
= inspect
.getsourcelines(m
.draw
)
65 for i
, s
in enumerate(src
):
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
])
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
87 clazz
= getattr(bpy
.types
, id)
89 except AttributeError:
93 def getmodule(opname
):
95 clazz
= getclazz(opname
)
100 modn
= clazz
.__module
__
103 line
= inspect
.getsourcelines(clazz
)[1]
109 if modn
== 'bpy.types':
112 elif modn
!= '__main__':
113 mod
= sys
.modules
[modn
].__file
__
118 return mod
, line
, addon
123 opsdir
= dir(bpy
.ops
)
124 for opmodname
in opsdir
:
125 opmod
= getattr(bpy
.ops
, opmodname
)
126 opmoddir
= dir(opmod
)
128 name
= opmodname
+ "." + o
129 clazz
= getclazz(name
)
130 #if (clazz is not None) :# and clazz.__module__ != 'bpy.types'):
134 # add own operator name too, since its not loaded yet when this is called
135 allops
.append("text.edit_operator")
140 return [(y
, y
, "", x
) for x
, y
in enumerate(l
)]
142 class OperatorEntry(PropertyGroup
):
144 label
: StringProperty(
150 path
: StringProperty(
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"
176 path
: StringProperty(
188 def show_text(self
, context
, path
, line
):
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
197 bpy
.ops
.text
.jump(ctx
, line
=line
)
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
):
213 exclude
.extend(sys
.stdlib_module_names
)
214 exclude
.append("bpy")
215 exclude
.append("sys")
218 walk_module(self
.op
, bl_ui
, calls
, exclude
)
220 for m
in addon_utils
.modules():
222 mod
= sys
.modules
[m
.__name
__]
223 walk_module(self
.op
, mod
, calls
, exclude
)
228 cl
= context
.scene
.calls
.add()
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
)
244 context
.scene
.calls
.clear()
245 path
, line
, addon
= getmodule(self
.op
)
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()
257 self
.show_calls(context
)
258 context
.area
.tag_redraw()
263 self
.report({'WARNING'},
264 "Found no source file for " + self
.op
)
266 self
.show_calls(context
)
267 context
.area
.tag_redraw()
272 class TEXT_PT_EditOperatorPanel(Panel
):
273 bl_space_type
= 'TEXT_EDITOR'
274 bl_region_type
= 'UI'
275 bl_label
= "Edit Operator"
277 bl_options
= {'DEFAULT_CLOSED'}
279 def draw(self
, context
):
281 op
= layout
.operator("text.edit_operator")
285 if len(context
.scene
.calls
) > 0:
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
)
297 bpy
.utils
.register_class(OperatorEntry
)
298 bpy
.types
.Scene
.calls
= bpy
.props
.CollectionProperty(name
="Calls",
300 bpy
.utils
.register_class(TEXT_OT_EditOperator
)
301 bpy
.utils
.register_class(TEXT_PT_EditOperatorPanel
)
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__":