mirror of https://github.com/H-uru/korman.git
Browse Source
Implements the boilerplate code for compiling Python code in arbitrary python versions and packing the marshalled data into Cyan's Python.pak format. Since this is a lot of bp, a separate operator has been added to both test the resulting mayhem and provide age creators an easy way to export only their needed Python. The only python that is packed currently is the age sdl hook file, if any. In order for that part to happen, Python File nodes need to be upgraded from having a string path to actually using the new text_id field.pull/128/head
Adam Johnson
6 years ago
13 changed files with 786 additions and 24 deletions
@ -0,0 +1,173 @@
|
||||
# This file is part of Korman. |
||||
# |
||||
# Korman 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 3 of the License, or |
||||
# (at your option) any later version. |
||||
# |
||||
# Korman 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 Korman. If not, see <http://www.gnu.org/licenses/>. |
||||
|
||||
import bpy |
||||
from pathlib import Path |
||||
from PyHSPlasma import * |
||||
|
||||
from .explosions import ExportError |
||||
from . import logger |
||||
from .. import korlib |
||||
from ..plasma_magic import plasma_python_glue, very_very_special_python |
||||
|
||||
class PythonPackageExporter: |
||||
def __init__(self, filepath, version): |
||||
self._filepath = filepath |
||||
self._modules = {} |
||||
self._pfms = {} |
||||
self._version = version |
||||
|
||||
def _compyle(self, report): |
||||
report.progress_advance() |
||||
report.progress_range = len(self._modules) + len(self._pfms) |
||||
inc_progress = report.progress_increment |
||||
|
||||
age = bpy.context.scene.world.plasma_age |
||||
Text = bpy.types.Text |
||||
if self._version <= pvPots: |
||||
py_version = (2, 2) |
||||
else: |
||||
py_version = (2, 3) |
||||
py_code = [] |
||||
|
||||
for filename, source in self._pfms.items(): |
||||
if isinstance(source, Text): |
||||
if not source.plasma_text.package and age.python_method != "all": |
||||
inc_progress() |
||||
continue |
||||
code = source.as_string() |
||||
else: |
||||
code = source |
||||
|
||||
code = "{}\n\n{}\n".format(code, plasma_python_glue) |
||||
success, result = korlib.compyle(filename, code, py_version, report, indent=1) |
||||
if not success: |
||||
raise ExportError("Failed to compyle '{}':\n{}".format(filename, result)) |
||||
py_code.append((filename, result)) |
||||
inc_progress() |
||||
|
||||
for filename, source in self._modules.items(): |
||||
if isinstance(source, Text): |
||||
if not source.plasma_text.package and age.python_method != "all": |
||||
inc_progress() |
||||
continue |
||||
code = source.as_string() |
||||
else: |
||||
code = source |
||||
|
||||
# no glue needed here, ma! |
||||
success, result = korlib.compyle(filename, code, py_version, report, indent=1) |
||||
if not success: |
||||
raise ExportError("Failed to compyle '{}':\n{}".format(filename, result)) |
||||
py_code.append((filename, result)) |
||||
inc_progress() |
||||
|
||||
# man that was ugly... |
||||
return py_code |
||||
|
||||
def _ensure_age_sdl_hook(self, report): |
||||
age_props = bpy.context.scene.world.plasma_age |
||||
if age_props.age_sdl: |
||||
py_filename = "{}.py".format(age_props.age_name) |
||||
age_py = self._modules.get(py_filename) |
||||
if age_py is not None: |
||||
del self._modules[py_filename] |
||||
if age_py.plasma_text.package or age.python_method == "all": |
||||
self._pfms[py_filename] = age_py |
||||
else: |
||||
report.warn("AgeSDL Python Script provided, but not requested for packing... Using default Python.", indent=1) |
||||
self._pfms[py_filename] = very_very_special_python.format(age_name=age_props.age_name) |
||||
else: |
||||
report.msg("Packing default AgeSDL Python", indent=1) |
||||
very_very_special_python.format(age_name=age_props.age_name) |
||||
self._pfms[py_filename] = very_very_special_python.format(age_name=age_props.age_name) |
||||
|
||||
def _harvest_pfms(self, report): |
||||
objects = bpy.context.scene.objects |
||||
report.progress_advance() |
||||
report.progress_range = len(objects) |
||||
inc_progress = report.progress_increment |
||||
|
||||
for i in objects: |
||||
logic = i.plasma_modifiers.advanced_logic |
||||
if i.plasma_object.enabled and logic.enabled: |
||||
for j in logic.logic_groups: |
||||
tree_versions = (globals()[version] for version in j.version) |
||||
if self._version in tree_versions: |
||||
self._harvest_tree(j.node_tree) |
||||
inc_progress() |
||||
|
||||
def _harvest_modules(self, report): |
||||
texts = bpy.data.texts |
||||
report.progress_advance() |
||||
report.progress_range = len(texts) |
||||
inc_progress = report.progress_increment |
||||
|
||||
for i in texts: |
||||
if i.name.endswith(".py") and i.name not in self._pfms: |
||||
self._modules.setdefault(i.name, i) |
||||
inc_progress() |
||||
|
||||
def _harvest_tree(self, tree): |
||||
# Search the node tree for any python file nodes. Any that we find are PFMs |
||||
for i in tree.nodes: |
||||
if i.bl_idname == "PlasmaPythonFileNode": |
||||
if i.filename and i.text_id: |
||||
self._pfms.setdefault(i.filename, i.text_id) |
||||
|
||||
def run(self): |
||||
"""Runs a stripped-down version of the Exporter that only handles Python files""" |
||||
age_props = bpy.context.scene.world.plasma_age |
||||
log = logger.ExportVerboseLogger if age_props.verbose else logger.ExportProgressLogger |
||||
with korlib.ConsoleToggler(age_props.show_console), log(self._filepath) as report: |
||||
report.progress_add_step("Harvesting Plasma PythonFileMods") |
||||
report.progress_add_step("Harvesting Helper Python Modules") |
||||
report.progress_add_step("Compyling Python Code") |
||||
report.progress_add_step("Packing Compyled Code") |
||||
report.progress_start("PACKING PYTHON") |
||||
|
||||
# Harvest the Python code |
||||
self._harvest_pfms(report) |
||||
self._harvest_modules(report) |
||||
self._ensure_age_sdl_hook(report) |
||||
|
||||
# Compyle and package the Python |
||||
self._package_python(report) |
||||
|
||||
# DONE |
||||
report.progress_end() |
||||
|
||||
def _package_python(self, report): |
||||
py_code = self._compyle(report) |
||||
self._write_python_pak(py_code, report) |
||||
|
||||
def _write_python_pak(self, py_code, report): |
||||
report.progress_advance() |
||||
|
||||
if self._version == pvEoa: |
||||
enc = plEncryptedStream.kEncAes |
||||
elif self._version == pvMoul: |
||||
enc = None |
||||
else: |
||||
enc = plEncryptedStream.kEncXtea |
||||
|
||||
if enc is None: |
||||
stream = hsFileStream(self._version).open(self._filepath, fmCreate) |
||||
else: |
||||
stream = plEncryptedStream(self._version).open(self._filepath, fmCreate, enc) |
||||
try: |
||||
korlib.package_python(stream, py_code) |
||||
finally: |
||||
stream.close() |
@ -0,0 +1,200 @@
|
||||
# This file is part of Korman. |
||||
# |
||||
# Korman 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 3 of the License, or |
||||
# (at your option) any later version. |
||||
# |
||||
# Korman 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 Korman. If not, see <http://www.gnu.org/licenses/>. |
||||
|
||||
from __future__ import generators # Python 2.2 |
||||
import marshal |
||||
import os.path |
||||
import sys |
||||
|
||||
_python_executables = {} |
||||
|
||||
class PythonNotAvailableError(Exception): |
||||
pass |
||||
|
||||
|
||||
def compyle(file_name, py_code, py_version, report=None, indent=0): |
||||
# NOTE: Should never run under Python 2.x |
||||
my_version = sys.version_info[:2] |
||||
assert my_version == (2, 7) or my_version[0] > 2 |
||||
|
||||
# Remember: Python 2.2 file, so no single line if statements... |
||||
idx = file_name.find('.') |
||||
if idx == -1: |
||||
module_name = file_name |
||||
else: |
||||
module_name = file_name[:idx] |
||||
|
||||
if report is not None: |
||||
report.msg("Compyling {}", file_name, indent=indent) |
||||
|
||||
if my_version != py_version: |
||||
import subprocess |
||||
|
||||
py_executable = _find_python(py_version) |
||||
args = (py_executable, __file__, module_name) |
||||
try: |
||||
py_code = py_code.encode("utf-8") |
||||
except UnicodeError: |
||||
if report is not None: |
||||
report.error("Could not encode '{}'", file_name, indent=indent+1) |
||||
return (False, "Could not encode file") |
||||
result = subprocess.run(args, input=py_code, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) |
||||
if result.returncode != 0: |
||||
try: |
||||
error = result.stdout.decode("utf-8").replace('\r\n', '\n') |
||||
except UnicodeError: |
||||
error = result.stdout |
||||
if report is not None: |
||||
report.error("Compylation Error in '{}'\n{}", file_name, error, indent=indent+1) |
||||
return (result.returncode == 0, result.stdout) |
||||
else: |
||||
raise NotImplementedError() |
||||
|
||||
def _compyle(module_name, py_code): |
||||
# Old python versions have major issues with Windows style newlines. |
||||
# Also, bad things happen if there is no newline at the end. |
||||
py_code += '\n' # sigh, this is slow on old Python... |
||||
py_code = py_code.replace('\r\n', '\n') |
||||
py_code = py_code.replace('\r', '\n') |
||||
code_object = compile(py_code, module_name, "exec") |
||||
|
||||
# The difference between us and the py_compile module is twofold: |
||||
# 1) py_compile compyles to a file. We might be exporting to memory, so that's |
||||
# not what we want. |
||||
# 2) py_compile saves a *pyc format file containing information such as compyle |
||||
# time and marshal format version. These items are not included in Cyan's |
||||
# Python.pak format. |
||||
# Therefore, we simply return the marshalled data as a string. |
||||
return marshal.dumps(code_object) |
||||
|
||||
def _find_python(py_version): |
||||
def find_executable(py_version): |
||||
# First, try to use Blender to find the Python executable |
||||
try: |
||||
import bpy |
||||
except ImportError: |
||||
pass |
||||
else: |
||||
userprefs = bpy.context.user_preferences.addons["korman"].preferences |
||||
py_executable = getattr(userprefs, "python{}{}_executable".format(*py_version), None) |
||||
if _verify_python(py_version, py_executable): |
||||
return py_executable |
||||
|
||||
# Second, try looking Python up in the registry. |
||||
try: |
||||
import winreg |
||||
except ImportError: |
||||
pass |
||||
else: |
||||
py_executable = _find_python_reg(winreg.HKEY_LOCAL_MACHINE, py_version) |
||||
if _verify_python(py_version, py_executable): |
||||
return py_executable |
||||
py_executable = _find_python_reg(winreg.HKEY_CURRENT_USER, py_version) |
||||
if _verify_python(py_version, py_executable): |
||||
return py_executable |
||||
|
||||
# I give up, you win. |
||||
return None |
||||
|
||||
py_executable = _python_executables.setdefault(py_version, find_executable(py_version)) |
||||
if py_executable: |
||||
return py_executable |
||||
else: |
||||
raise PythonNotAvailableError("{}.{}".format(*py_version)) |
||||
|
||||
def _find_python_reg(reg_key, py_version): |
||||
import winreg |
||||
subkey_name = "Software\\Python\\PythonCore\\{}.{}\\InstallPath".format(*py_version) |
||||
try: |
||||
python_dir = winreg.QueryValue(reg_key, subkey_name) |
||||
except FileNotFoundError: |
||||
return None |
||||
else: |
||||
return os.path.join(python_dir, "python.exe") |
||||
|
||||
def package_python(stream, pyc_objects): |
||||
# Python.pak format: |
||||
# uint32_t numFiles |
||||
# - safeStr filename |
||||
# - uint32_t offset |
||||
# ~~~~~ |
||||
# uint32_t filesz |
||||
# uint8_t data[filesz] |
||||
assert bool(pyc_objects) |
||||
|
||||
# `stream` might be a plEncryptedStream, which doesn't seek very well at all. |
||||
# Therefore, we will go ahead and calculate the size of the index block so |
||||
# there is no need to seek around to write offset values |
||||
base_offset = 4 # uint32_t numFiles |
||||
data_offset = 0 |
||||
pyc_info = [] # sad, but makes life easier... |
||||
for module_name, compyled_code in pyc_objects: |
||||
pyc_info.append((module_name, data_offset, compyled_code)) |
||||
|
||||
# index offset overall |
||||
base_offset += 2 # writeSafeStr length |
||||
# NOTE: This assumes that libHSPlasma's hsStream::writeSafeStr converts |
||||
# the Python unicode/string object to UTF-8. Currently, this is true. |
||||
base_offset += len(module_name.encode("utf-8")) # writeSafeStr |
||||
base_offset += 4 |
||||
|
||||
# current file data offset |
||||
data_offset += 4 # uint32_t filesz |
||||
data_offset += len(compyled_code) |
||||
|
||||
stream.writeInt(len(pyc_info)) |
||||
for module_name, data_offset, compyled_code in pyc_info: |
||||
stream.writeSafeStr(module_name) |
||||
# offset of data == index size (base_offset) + offset to data blob (data_offset) |
||||
stream.writeInt(base_offset + data_offset) |
||||
for module_name, data_offset, compyled_code in pyc_info: |
||||
stream.writeInt(len(compyled_code)) |
||||
stream.write(compyled_code) |
||||
|
||||
def _verify_python(py_version, py_exe): |
||||
if not py_exe: |
||||
return False |
||||
|
||||
import subprocess |
||||
try: |
||||
args = (py_exe, "-V") |
||||
result = subprocess.run(args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, timeout=5) |
||||
except OSError: |
||||
return False |
||||
else: |
||||
output = result.stdout.decode() |
||||
try: |
||||
py_str, py_check = output[:6], output[7:10] |
||||
except IndexError: |
||||
return False |
||||
else: |
||||
if py_str != "Python": |
||||
return False |
||||
return "{}.{}".format(*py_version) == py_check |
||||
|
||||
if __name__ == "__main__": |
||||
# Python tries to be "helpful" on Windows by converting \n to \r\n. |
||||
# Therefore we must change the mode of stdout. |
||||
if sys.platform == "win32": |
||||
import os, msvcrt |
||||
msvcrt.setmode(sys.stdout.fileno(), os.O_BINARY) |
||||
|
||||
try: |
||||
module_name = sys.argv[1] |
||||
except IndexError: |
||||
module_name = "<string>" |
||||
py_code_source = sys.stdin.read() |
||||
py_code_object = _compyle(module_name, py_code_source) |
||||
sys.stdout.write(py_code_object) |
@ -0,0 +1,219 @@
|
||||
# This file is part of Korman. |
||||
# |
||||
# Korman 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 3 of the License, or |
||||
# (at your option) any later version. |
||||
# |
||||
# Korman 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 Korman. If not, see <http://www.gnu.org/licenses/>. |
||||
|
||||
very_very_special_python = """ |
||||
from Plasma import * |
||||
from PlasmaTypes import * |
||||
|
||||
globals()["{age_name}"] = type("{age_name}", (ptResponder,), dict()) |
||||
""" |
||||
|
||||
very_very_special_sdl = """ |
||||
#============================================================== |
||||
# This VeryVerySpecial SDL File was automatically generated |
||||
# by Korman. Have a nice day! |
||||
# |
||||
# READ: When modifying an SDL record, do *not* modify the |
||||
# existing record. You must copy and paste a new version |
||||
# below the current one and make your changes there. |
||||
#============================================================== |
||||
|
||||
STATEDESC {age_name} |
||||
{{ |
||||
VERSION 0 |
||||
}} |
||||
|
||||
""" |
||||
|
||||
# Copypasta (with small fixes for str.format) of glue.py from CWE's moul-scripts |
||||
plasma_python_glue = """ |
||||
glue_cl = None |
||||
glue_inst = None |
||||
glue_params = None |
||||
glue_paramKeys = None |
||||
try: |
||||
x = glue_verbose |
||||
except NameError: |
||||
glue_verbose = 0 |
||||
def glue_getClass(): |
||||
global glue_cl |
||||
if glue_cl == None: |
||||
try: |
||||
cl = globals()[glue_name] |
||||
if issubclass(cl,ptModifier): |
||||
glue_cl = cl |
||||
else: |
||||
if glue_verbose: |
||||
print "Class %s is not derived from modifier" % (cl.__name__) |
||||
except: |
||||
if glue_verbose: |
||||
try: |
||||
print "Could not find class %s" % (glue_name) |
||||
except NameError: |
||||
print "Filename/classname not set!" |
||||
return glue_cl |
||||
def glue_getInst(): |
||||
global glue_inst |
||||
if type(glue_inst) == type(None): |
||||
cl = glue_getClass() |
||||
if cl != None: |
||||
glue_inst = cl() |
||||
return glue_inst |
||||
def glue_delInst(): |
||||
global glue_inst |
||||
global glue_cl |
||||
global glue_params |
||||
global glue_paramKeys |
||||
if type(glue_inst) != type(None): |
||||
del glue_inst |
||||
glue_cl = None |
||||
glue_params = None |
||||
glue_paramKeys = None |
||||
def glue_getVersion(): |
||||
inst = glue_getInst() |
||||
ver = inst.version |
||||
glue_delInst() |
||||
return ver |
||||
def glue_findAndAddAttribs(obj, glue_params): |
||||
if isinstance(obj,ptAttribute): |
||||
if glue_params.has_key(obj.id): |
||||
if glue_verbose: |
||||
print "WARNING: Duplicate attribute ids!" |
||||
print "%s has id %d which is already defined in %s" % (obj.name, obj.id, glue_params[obj.id].name) |
||||
else: |
||||
glue_params[obj.id] = obj |
||||
elif type(obj) == type([]): |
||||
for o in obj: |
||||
glue_findAndAddAttribs(o, glue_params) |
||||
elif type(obj) == type(dict()): |
||||
for o in obj.values(): |
||||
glue_findAndAddAttribs(o, glue_params) |
||||
elif type(obj) == type( () ): |
||||
for o in obj: |
||||
glue_findAndAddAttribs(o, glue_params) |
||||
|
||||
def glue_getParamDict(): |
||||
global glue_params |
||||
global glue_paramKeys |
||||
if type(glue_params) == type(None): |
||||
glue_params = dict() |
||||
gd = globals() |
||||
for obj in gd.values(): |
||||
glue_findAndAddAttribs(obj, glue_params) |
||||
# rebuild the parameter sorted key list |
||||
glue_paramKeys = glue_params.keys() |
||||
glue_paramKeys.sort() |
||||
glue_paramKeys.reverse() |
||||
return glue_params |
||||
def glue_getClassName(): |
||||
cl = glue_getClass() |
||||
if cl != None: |
||||
return cl.__name__ |
||||
if glue_verbose: |
||||
print "Class not found in %s.py" % (glue_name) |
||||
return None |
||||
def glue_getBlockID(): |
||||
inst = glue_getInst() |
||||
if inst != None: |
||||
return inst.id |
||||
if glue_verbose: |
||||
print "Instance could not be created in %s.py" % (glue_name) |
||||
return None |
||||
def glue_getNumParams(): |
||||
pd = glue_getParamDict() |
||||
if pd != None: |
||||
return len(pd) |
||||
if glue_verbose: |
||||
print "No attributes found in %s.py" % (glue_name) |
||||
return 0 |
||||
def glue_getParam(number): |
||||
global glue_paramKeys |
||||
pd = glue_getParamDict() |
||||
if pd != None: |
||||
# see if there is a paramKey list |
||||
if type(glue_paramKeys) == type([]): |
||||
if number >= 0 and number < len(glue_paramKeys): |
||||
return pd[glue_paramKeys[number]].getdef() |
||||
else: |
||||
print "glue_getParam: Error! %d out of range of attribute list" % (number) |
||||
else: |
||||
pl = pd.values() |
||||
if number >= 0 and number < len(pl): |
||||
return pl[number].getdef() |
||||
else: |
||||
if glue_verbose: |
||||
print "glue_getParam: Error! %d out of range of attribute list" % (number) |
||||
if glue_verbose: |
||||
print "GLUE: Attribute list error" |
||||
return None |
||||
def glue_setParam(id,value): |
||||
pd = glue_getParamDict() |
||||
if pd != None: |
||||
if pd.has_key(id): |
||||
try: |
||||
pd[id].__setvalue__(value) |
||||
except AttributeError: |
||||
if isinstance(pd[id],ptAttributeList): |
||||
try: |
||||
if type(pd[id].value) != type([]): |
||||
pd[id].value = [] |
||||
except AttributeError: |
||||
pd[id].value = [] |
||||
pd[id].value.append(value) |
||||
else: |
||||
pd[id].value = value |
||||
else: |
||||
if glue_verbose: |
||||
print "setParam: can't find id=",id |
||||
else: |
||||
print "setParma: Something terribly has gone wrong. Head for the cover." |
||||
def glue_isNamedAttribute(id): |
||||
pd = glue_getParamDict() |
||||
if pd != None: |
||||
try: |
||||
if isinstance(pd[id],ptAttribNamedActivator): |
||||
return 1 |
||||
if isinstance(pd[id],ptAttribNamedResponder): |
||||
return 2 |
||||
except KeyError: |
||||
if glue_verbose: |
||||
print "Could not find id=%d attribute" % (id) |
||||
return 0 |
||||
def glue_isMultiModifier(): |
||||
inst = glue_getInst() |
||||
if isinstance(inst,ptMultiModifier): |
||||
return 1 |
||||
return 0 |
||||
def glue_getVisInfo(number): |
||||
global glue_paramKeys |
||||
pd = glue_getParamDict() |
||||
if pd != None: |
||||
# see if there is a paramKey list |
||||
if type(glue_paramKeys) == type([]): |
||||
if number >= 0 and number < len(glue_paramKeys): |
||||
return pd[glue_paramKeys[number]].getVisInfo() |
||||
else: |
||||
print "glue_getVisInfo: Error! %d out of range of attribute list" % (number) |
||||
else: |
||||
pl = pd.values() |
||||
if number >= 0 and number < len(pl): |
||||
return pl[number].getVisInfo() |
||||
else: |
||||
if glue_verbose: |
||||
print "glue_getVisInfo: Error! %d out of range of attribute list" % (number) |
||||
if glue_verbose: |
||||
print "GLUE: Attribute list error" |
||||
return None |
||||
""" |
@ -0,0 +1,22 @@
|
||||
# This file is part of Korman. |
||||
# |
||||
# Korman 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 3 of the License, or |
||||
# (at your option) any later version. |
||||
# |
||||
# Korman 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 Korman. If not, see <http://www.gnu.org/licenses/>. |
||||
|
||||
import bpy |
||||
from bpy.props import * |
||||
|
||||
class PlasmaText(bpy.types.PropertyGroup): |
||||
package = BoolProperty(name="Export", |
||||
description="Package this file in the age export", |
||||
options=set()) |
@ -0,0 +1,28 @@
|
||||
# This file is part of Korman. |
||||
# |
||||
# Korman 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 3 of the License, or |
||||
# (at your option) any later version. |
||||
# |
||||
# Korman 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 Korman. If not, see <http://www.gnu.org/licenses/>. |
||||
|
||||
import bpy |
||||
|
||||
class PlasmaTextEditorHeader(bpy.types.Header): |
||||
bl_space_type = "TEXT_EDITOR" |
||||
|
||||
def draw(self, context): |
||||
layout, text = self.layout, context.space_data.text |
||||
|
||||
if text is not None: |
||||
is_py = text.name.endswith(".py") |
||||
row = layout.row(align=True) |
||||
row.enabled = is_py |
||||
row.prop(text.plasma_text, "package") |
Loading…
Reference in new issue