1 # SPDX-FileCopyrightText: 2017-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
7 "name": "Edit Operator Source",
8 "author": "scorpion81",
11 "location": "Text Editor > Sidebar > Edit Operator",
12 "description": "Opens source file of chosen operator or call locations, if source not available",
14 "doc_url": "{BLENDER_MANUAL_URL}/addons/development/edit_operator.html",
15 "category": "Development",
22 from bpy
.types
import (
29 from bpy
.props
import (
35 def make_loc(prefix
, c
):
36 #too long and not helpful... omitting for now
38 #if hasattr(c, "bl_space_type"):
39 # space = c.bl_space_type
42 #if hasattr(c, "bl_region_type"):
43 # region = c.bl_region_type
46 if hasattr(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"):
66 src
, n
= inspect
.getsourcelines(m
.draw
)
67 for i
, s
in enumerate(src
):
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
])
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
89 clazz
= getattr(bpy
.types
, id)
91 except AttributeError:
95 def getmodule(opname
):
97 clazz
= getclazz(opname
)
102 modn
= clazz
.__module
__
105 line
= inspect
.getsourcelines(clazz
)[1]
111 if modn
== 'bpy.types':
114 elif modn
!= '__main__':
115 mod
= sys
.modules
[modn
].__file
__
120 return mod
, line
, addon
125 opsdir
= dir(bpy
.ops
)
126 for opmodname
in opsdir
:
127 opmod
= getattr(bpy
.ops
, opmodname
)
128 opmoddir
= dir(opmod
)
130 name
= opmodname
+ "." + o
131 clazz
= getclazz(name
)
132 #if (clazz is not None) :# and clazz.__module__ != 'bpy.types'):
136 # add own operator name too, since its not loaded yet when this is called
137 allops
.append("text.edit_operator")
142 return [(y
, y
, "", x
) for x
, y
in enumerate(l
)]
144 class OperatorEntry(PropertyGroup
):
146 label
: StringProperty(
152 path
: StringProperty(
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"
178 path
: StringProperty(
190 def show_text(self
, context
, path
, line
):
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
199 bpy
.ops
.text
.jump(ctx
, line
=line
)
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
):
215 exclude
.extend(sys
.stdlib_module_names
)
216 exclude
.append("bpy")
217 exclude
.append("sys")
220 walk_module(self
.op
, bl_ui
, calls
, exclude
)
222 for m
in addon_utils
.modules():
224 mod
= sys
.modules
[m
.__name
__]
225 walk_module(self
.op
, mod
, calls
, exclude
)
230 cl
= context
.scene
.calls
.add()
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
)
246 context
.scene
.calls
.clear()
247 path
, line
, addon
= getmodule(self
.op
)
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()
259 self
.show_calls(context
)
260 context
.area
.tag_redraw()
265 self
.report({'WARNING'},
266 "Found no source file for " + self
.op
)
268 self
.show_calls(context
)
269 context
.area
.tag_redraw()
274 class TEXT_PT_EditOperatorPanel(Panel
):
275 bl_space_type
= 'TEXT_EDITOR'
276 bl_region_type
= 'UI'
277 bl_label
= "Edit Operator"
279 bl_options
= {'DEFAULT_CLOSED'}
281 def draw(self
, context
):
283 op
= layout
.operator("text.edit_operator")
287 if len(context
.scene
.calls
) > 0:
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
)
299 bpy
.utils
.register_class(OperatorEntry
)
300 bpy
.types
.Scene
.calls
= bpy
.props
.CollectionProperty(name
="Calls",
302 bpy
.utils
.register_class(TEXT_OT_EditOperator
)
303 bpy
.utils
.register_class(TEXT_PT_EditOperatorPanel
)
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__":