Newer
Older
# ##### BEGIN GPL LICENSE BLOCK #####
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# ##### END GPL LICENSE BLOCK #####
bl_info = {
"name": "Edit Operator Source",
"author": "scorpion81",
"version": (1, 2, 2),
"blender": (2, 80, 0),
"location": "Text Editor > Sidebar > Edit Operator",
"description": "Opens source file of chosen operator or call locations, if source not available",
"warning": "",
"wiki_url": "https://docs.blender.org/manual/en/dev/addons/"
"development/edit_operator.html",
"category": "Development"}
import bpy
import sys
import os
import inspect
from bpy.types import (
Operator,
Panel,
Header,
Menu,
PropertyGroup
)
EnumProperty,
StringProperty,
IntProperty
)
def stdlib_excludes():
#need a handy list of modules to avoid walking into
import distutils.sysconfig as sysconfig
excludes = []
std_lib = sysconfig.get_python_lib(standard_lib=True)
for top, dirs, files in os.walk(std_lib):
for nm in files:
if nm != '__init__.py' and nm[-3:] == '.py':
excludes.append(os.path.join(top, nm)[len(std_lib)+1:-3].replace('\\','.'))
return excludes
def make_loc(prefix, c):
#too long and not helpful... omitting for now
space = ""
#if hasattr(c, "bl_space_type"):
# space = c.bl_space_type
region = ""
#if hasattr(c, "bl_region_type"):
# region = c.bl_region_type
label = ""
if hasattr(c, "bl_label"):
label = c.bl_label
return prefix+": " + space + " " + region + " " + label
def walk_module(opname, mod, calls=[], exclude=[]):
for name, m in inspect.getmembers(mod):
if inspect.ismodule(m):
if m.__name__ not in exclude:
#print(name, m.__name__)
walk_module(opname, m, calls, exclude)
elif inspect.isclass(m):
if (issubclass(m, Panel) or \
issubclass(m, Header) or \
issubclass(m, Menu)) and mod.__name__ != "bl_ui":
if hasattr(m, "draw"):
loc = ""
file = ""
line = -1
src, n = inspect.getsourcelines(m.draw)
for i, s in enumerate(src):
if opname in s:
file = mod.__file__
line = n + i
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
if issubclass(m, Panel) and name != "Panel":
loc = make_loc("Panel", m)
calls.append([opname, loc, file, line])
if issubclass(m, Header) and name != "Header":
loc = make_loc("Header", m)
calls.append([opname, loc, file, line])
if issubclass(m, Menu) and name != "Menu":
loc = make_loc("Menu", m)
calls.append([opname, loc, file, line])
def getclazz(opname):
opid = opname.split(".")
opmod = getattr(bpy.ops, opid[0])
op = getattr(opmod, opid[1])
id = op.get_rna_type().bl_rna.identifier
try:
clazz = getattr(bpy.types, id)
return clazz
except AttributeError:
return None
def getmodule(opname):
addon = True
clazz = getclazz(opname)
if clazz is None:
return "", -1, False
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
modn = clazz.__module__
try:
line = inspect.getsourcelines(clazz)[1]
except IOError:
line = -1
except TypeError:
line = -1
if modn == 'bpy.types':
mod = 'C operator'
addon = False
elif modn != '__main__':
mod = sys.modules[modn].__file__
else:
addon = False
mod = modn
return mod, line, addon
def get_ops():
allops = []
opsdir = dir(bpy.ops)
for opmodname in opsdir:
opmod = getattr(bpy.ops, opmodname)
opmoddir = dir(opmod)
for o in opmoddir:
name = opmodname + "." + o
clazz = getclazz(name)
#if (clazz is not None) :# and clazz.__module__ != 'bpy.types'):
allops.append(name)
del opmoddir
# add own operator name too, since its not loaded yet when this is called
allops.append("text.edit_operator")
l = sorted(allops)
del allops
del opsdir
return [(y, y, "", x) for x, y in enumerate(l)]
class OperatorEntry(PropertyGroup):
label : StringProperty(
name="Label",
description="",
default=""
)
path : StringProperty(
name="Path",
description="",
default=""
)
line : IntProperty(
name="Line",
description="",
default=-1
class TEXT_OT_EditOperator(Operator):
bl_idname = "text.edit_operator"
bl_label = "Edit Operator"
bl_description = "Opens the source file of operators chosen from Menu"
bl_property = "op"
items = get_ops()
op : EnumProperty(
name="Op",
description="",
items=items
)
path : StringProperty(
name="Path",
description="",
default=""
)
line : IntProperty(
name="Line",
description="",
default=-1
)
def show_text(self, context, path, line):
found = False
for t in bpy.data.texts:
if t.filepath == path:
#switch to the wanted text first
context.space_data.text = t
ctx = context.copy()
ctx['edit_text'] = t
bpy.ops.text.jump(ctx, line=line)
found = True
break
if (found is False):
self.report({'INFO'},
"Opened file: " + path)
bpy.ops.text.open(filepath=path)
bpy.ops.text.jump(line=line)
def show_calls(self, context):
import bl_ui
import addon_utils
exclude = stdlib_excludes()
exclude.append("bpy")
exclude.append("sys")
calls = []
walk_module(self.op, bl_ui, calls, exclude)
for m in addon_utils.modules():
try:
mod = sys.modules[m.__name__]
walk_module(self.op, mod, calls, exclude)
except KeyError:
continue
for c in calls:
cl = context.scene.calls.add()
cl.name = c[0]
cl.label = c[1]
cl.path = c[2]
cl.line = c[3]
def invoke(self, context, event):
context.window_manager.invoke_search_popup(self)
return {'PASS_THROUGH'}
def execute(self, context):
if self.path != "" and self.line != -1:
#invocation of one of the "found" locations
self.show_text(context, self.path, self.line)
else:
context.scene.calls.clear()
path, line, addon = getmodule(self.op)
if addon:
self.show_text(context, path, line)
#add convenient "source" button, to toggle back from calls to source
c = context.scene.calls.add()
c.name = self.op
c.label = "Source"
c.path = path
c.line = line
self.show_calls(context)
context.area.tag_redraw()
return {'FINISHED'}
else:
self.report({'WARNING'},
"Found no source file for " + self.op)
self.show_calls(context)
context.area.tag_redraw()
return {'FINISHED'}
class TEXT_PT_EditOperatorPanel(Panel):
bl_space_type = 'TEXT_EDITOR'
bl_region_type = 'UI'
bl_label = "Edit Operator"
bl_category = "Text"
bl_options = {'DEFAULT_CLOSED'}
def draw(self, context):
layout = self.layout
op = layout.operator("text.edit_operator")
op.path = ""
op.line = -1
if len(context.scene.calls) > 0:
box = layout.box()
box.label(text="Calls of: " + context.scene.calls[0].name)
box.operator_context = 'EXEC_DEFAULT'
for c in context.scene.calls:
op = box.operator("text.edit_operator", text=c.label)
op.path = c.path
op.line = c.line
op.op = c.name
def register():
bpy.utils.register_class(OperatorEntry)
bpy.types.Scene.calls = bpy.props.CollectionProperty(name="Calls",
type=OperatorEntry)
bpy.utils.register_class(TEXT_OT_EditOperator)
bpy.utils.register_class(TEXT_PT_EditOperatorPanel)
def unregister():
bpy.utils.unregister_class(TEXT_PT_EditOperatorPanel)
bpy.utils.unregister_class(TEXT_OT_EditOperator)
del bpy.types.Scene.calls
bpy.utils.unregister_class(OperatorEntry)
if __name__ == "__main__":
register()