diff --git a/leenkx.py b/leenkx.py
index d0960cf0..dc2dac68 100644
--- a/leenkx.py
+++ b/leenkx.py
@@ -12,6 +12,7 @@ bl_info = {
"tracker_url": "https://leenkx.com/support"
}
from enum import IntEnum
+import json
import os
from pathlib import Path
import platform
@@ -32,6 +33,41 @@ from bpy.app.handlers import persistent
from bpy.props import *
from bpy.types import Operator, AddonPreferences
+LEENKX_PREFS_ENV = 'LEENKX_ADDON_PREFS'
+
+persist_props = [
+ 'apply_theme', 'show_advanced', 'sdk_path', 'tabs',
+ 'code_editor', 'ide_bin', 'ui_scale', 'viewport_controls',
+ 'khamake_threads', 'khamake_threads_use_auto', 'compilation_server',
+ 'renderdoc_path', 'ffmpeg_path', 'save_on_build', 'open_build_directory',
+ 'cmft_use_opencl', 'legacy_shaders', 'relative_paths',
+ 'debug_console_auto', 'debug_console_visible_sc',
+ 'debug_console_scale_in_sc', 'debug_console_scale_out_sc',
+ 'android_sdk_root_path', 'android_open_build_apk_directory',
+ 'android_apk_copy_path', 'android_apk_copy_open_directory',
+ 'html5_copy_path', 'html5_server_port',
+ 'html5_server_log', 'profile_exporter', 'khamake_debug',
+ 'haxe_times', 'use_leenkx_py_symlink', 'update_submodules',
+]
+
+
+def save_prefs():
+ prefs = LeenkxAddonPreferences.get_prefs()
+ data = {k: getattr(prefs, k) for k in persist_props}
+ os.environ[LEENKX_PREFS_ENV] = json.dumps(data)
+
+
+def restore_prefs():
+ raw = os.environ.get(LEENKX_PREFS_ENV)
+ if not raw:
+ return
+ data = json.loads(raw)
+ prefs = LeenkxAddonPreferences.get_prefs()
+ prefs.skip_update = True
+ for k, v in data.items():
+ if hasattr(prefs, k):
+ setattr(prefs, k, v)
+
if bpy.app.version < (2, 90, 0):
ListType = List
@@ -116,7 +152,9 @@ class LeenkxAddonPreferences(AddonPreferences):
return
self.skip_update = True
self.sdk_path = bpy.path.reduce_dirs([bpy.path.abspath(self.sdk_path)])[0] + '/'
+ save_prefs()
restart_leenkx(context)
+ update_theme(context)
def ide_bin_update(self, context):
if self.skip_update:
@@ -311,6 +349,11 @@ class LeenkxAddonPreferences(AddonPreferences):
" development. Warning: this will invalidate the installation if the SDK is removed"),
update=lambda self, context: update_leenkx_py(get_sdk_path(context)),
)
+ apply_theme: BoolProperty(
+ name="Apply Leenkx Theme", default=True,
+ description="Automatically apply the Leenkx Blender theme on addon registration",
+ update=lambda self, context: update_theme(context),
+ )
def draw(self, context):
self.skip_update = False
@@ -450,6 +493,7 @@ class LeenkxAddonPreferences(AddonPreferences):
col = box.column(align=True)
col.prop(self, "use_leenkx_py_symlink")
+ col.prop(self, "apply_theme")
@staticmethod
def get_prefs() -> 'LeenkxAddonPreferences':
@@ -458,9 +502,10 @@ class LeenkxAddonPreferences(AddonPreferences):
def get_fp():
- if bpy.data.filepath == '':
+ filepath = getattr(bpy.data, 'filepath', '')
+ if filepath == '':
return ''
- s = bpy.data.filepath.split(os.path.sep)
+ s = filepath.split(os.path.sep)
s.pop()
return os.path.sep.join(s)
@@ -934,13 +979,47 @@ def on_load_post(context):
bpy.app.timers.register(lambda: restart_leenkx(bpy.context), first_interval=0.1)
+def remove_leenkx_theme():
+ if "leenkx.theme" in sys.modules:
+ sys.modules["leenkx.theme"].unregister()
+ del sys.modules["leenkx.theme"]
+
+
+def apply_leenkx_theme(sdk_path: str):
+ remove_leenkx_theme()
+ import importlib.util
+ theme_path = os.path.join(sdk_path, "leenkx", "blender", "theme", "theme.py")
+ if os.path.exists(theme_path):
+ spec = importlib.util.spec_from_file_location(
+ "leenkx.theme", theme_path)
+ theme = importlib.util.module_from_spec(spec)
+ sys.modules["leenkx.theme"] = theme
+ spec.loader.exec_module(theme)
+ theme.register(sdk_path)
+ else:
+ print("Leenkx theme: theme.py not found at", theme_path)
+
+
+def update_theme(context):
+ prefs = LeenkxAddonPreferences.get_prefs()
+ if prefs.apply_theme:
+ sdk_path = get_sdk_path(context)
+ if sdk_path != "":
+ apply_leenkx_theme(sdk_path)
+ else:
+ remove_leenkx_theme()
+
+
def on_register_post():
detect_sdk_path()
+ save_prefs()
restart_leenkx(bpy.context)
+ update_theme(bpy.context)
def register():
bpy.utils.register_class(LeenkxAddonPreferences)
+ restore_prefs()
bpy.utils.register_class(LnxAddonPrintVersionInfoButton)
bpy.utils.register_class(LnxAddonInstallButton)
bpy.utils.register_class(LnxAddonUpdateButton)
@@ -953,7 +1032,11 @@ def register():
def unregister():
+ if bpy.app.timers.is_registered(on_register_post):
+ bpy.app.timers.unregister(on_register_post)
+ remove_leenkx_theme()
stop_leenkx()
+ save_prefs()
bpy.utils.unregister_class(LeenkxAddonPreferences)
bpy.utils.unregister_class(LnxAddonInstallButton)
bpy.utils.unregister_class(LnxAddonPrintVersionInfoButton)
diff --git a/leenkx/blender/lnx/props_properties.py b/leenkx/blender/lnx/props_properties.py
index 765796cc..d514ac38 100644
--- a/leenkx/blender/lnx/props_properties.py
+++ b/leenkx/blender/lnx/props_properties.py
@@ -256,7 +256,10 @@ def register():
def unregister():
for cls in reversed(__REG_CLASSES):
- bpy.utils.unregister_class(cls)
+ try:
+ bpy.utils.unregister_class(cls)
+ except RuntimeError:
+ pass
del bpy.types.Object.lnx_propertylist
del bpy.types.Object.lnx_propertylist_index
del bpy.types.Scene.lnx_propertylist
diff --git a/leenkx/blender/theme/BlenderLeenkxTheme.xml b/leenkx/blender/theme/BlenderLeenkxTheme.xml
new file mode 100644
index 00000000..9d242007
--- /dev/null
+++ b/leenkx/blender/theme/BlenderLeenkxTheme.xml
@@ -0,0 +1,1271 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/leenkx/blender/theme/LeenkxLogo.png b/leenkx/blender/theme/LeenkxLogo.png
new file mode 100644
index 00000000..4ce54fb5
Binary files /dev/null and b/leenkx/blender/theme/LeenkxLogo.png differ
diff --git a/leenkx/blender/theme/blender_manifest.toml b/leenkx/blender/theme/blender_manifest.toml
new file mode 100644
index 00000000..f786a926
--- /dev/null
+++ b/leenkx/blender/theme/blender_manifest.toml
@@ -0,0 +1,10 @@
+schema_version = "1.0.0"
+id = "Leenkx_Theme"
+name = "Leenkx Theme"
+version = "1.0.0"
+tagline = "Leenkx theme for Blender"
+maintainer = "Onek8 "
+type = "theme"
+tags = ["Dark","Leenkx", "Solarized"]
+blender_version_min = "5.0.0"
+license = ["SPDX:GPL-2.0-or-later"]
diff --git a/leenkx/blender/theme/splash.png b/leenkx/blender/theme/splash.png
new file mode 100644
index 00000000..5acedf0e
Binary files /dev/null and b/leenkx/blender/theme/splash.png differ
diff --git a/leenkx/blender/theme/theme.py b/leenkx/blender/theme/theme.py
new file mode 100644
index 00000000..8d7b61fd
--- /dev/null
+++ b/leenkx/blender/theme/theme.py
@@ -0,0 +1,240 @@
+import os
+import traceback
+import bpy
+from bpy.types import Menu
+
+preview_collection = None
+logo_icon_id = 0
+splash_icon_id = 0
+original_splash_cls = None
+
+
+def load_icons():
+ global preview_collection, logo_icon_id, splash_icon_id
+ if preview_collection is not None:
+ return
+ preview_collection = bpy.utils.previews.new()
+ theme_dir = os.path.dirname(__file__)
+ logo_path = os.path.join(theme_dir, "LeenkxLogo.png")
+ splash_path = os.path.join(theme_dir, "splash.png")
+ if os.path.exists(logo_path):
+ preview_collection.load("logo", logo_path, 'IMAGE', force_reload=True)
+ logo_icon_id = preview_collection["logo"].icon_id
+ else:
+ print("Leenkx theme: logo not found at", logo_path)
+ if os.path.exists(splash_path):
+ preview_collection.load("splash", splash_path, 'IMAGE', force_reload=True)
+ splash_icon_id = preview_collection["splash"].icon_id
+ else:
+ print("Leenkx theme: splash not found at", splash_path)
+
+
+def install_theme(sdk_path: str):
+ theme_xml = os.path.join(sdk_path, "leenkx", "blender",
+ "theme", "BlenderLeenkxTheme.xml")
+ if not os.path.exists(theme_xml):
+ print("Leenkx theme: theme XML not found at", theme_xml)
+ return
+
+ bpy.ops.preferences.theme_install(
+ filepath=theme_xml, overwrite=True)
+
+
+def setup_splash(sdk_path: str):
+ splash_png = os.path.join(sdk_path, "leenkx", "blender",
+ "theme", "splash.png")
+ if os.path.exists(splash_png):
+ os.environ["BLENDER_CUSTOM_SPLASH"] = splash_png
+ else:
+ print("Leenkx splash: splash.png not found at", splash_png)
+
+
+class LEENKX_MT_splash(Menu):
+ bl_label = "Splash"
+ bl_idname = "WM_MT_splash"
+
+ def draw(self, context):
+ layout = self.layout
+ layout.operator_context = 'EXEC_DEFAULT'
+ layout.emboss = 'PULLDOWN_MENU'
+
+ split = layout.split()
+
+ col1 = split.column()
+ col1.label(text="New File")
+
+ bpy.types.TOPBAR_MT_file_new.draw_ex(col1, context, use_splash=True)
+
+ col2 = split.column()
+ col2_title = col2.row()
+
+ found_recent = col2.template_recent_files(rows=5)
+
+ if found_recent:
+ col2_title.label(text="Recent Files")
+
+ col_more = col2.column()
+ col_more.operator_context = 'INVOKE_DEFAULT'
+ more_props = col_more.operator("wm.search_single_menu", text="More...", icon='VIEWZOOM')
+ more_props.menu_idname = "TOPBAR_MT_file_open_recent"
+ else:
+ col2_title.label(text="Getting Started")
+
+ col2.operator("wm.url_open", text="Leenkx Docs",
+ icon='URL').url = "https://leenkx.com/docs"
+ col2.operator("wm.url_open", text="Support",
+ icon='URL').url = "https://leenkx.com/support"
+ col2.operator("wm.url_open", text="Community",
+ icon='URL').url = "https://leenkx.com/community"
+ col2.operator("wm.url_open", text="GitHub",
+ icon='URL').url = "https://github.com/leenkx/leenkx"
+
+ col_sep = layout.column()
+ col_sep.separator()
+ col_sep.separator(type='LINE')
+ col_sep.separator()
+
+ split = layout.split()
+
+ col1 = split.column()
+ sub = col1.row()
+ sub.operator_context = 'INVOKE_DEFAULT'
+ sub.operator("wm.open_mainfile", text="Open...", icon='FILE_FOLDER')
+ col1.operator("wm.recover_last_session", icon='RECOVER_LAST')
+
+ col2 = split.column()
+
+ col2.operator("wm.url_open", text="Support Leenkx",
+ icon='FUND').url = "https://leenkx.com/donate"
+ col2.operator("wm.url_open", text="Leenkx Website",
+ icon='URL').url = "https://leenkx.com"
+
+ layout.separator()
+
+ if (not bpy.app.online_access) and bpy.app.online_access_override:
+ self.layout.label(text="Running in Offline Mode", icon='INTERNET_OFFLINE')
+
+ layout.separator()
+
+
+def register_splash_menu():
+ global original_splash_cls
+ cls = getattr(bpy.types, "WM_MT_splash", None)
+ if cls is not None and cls is not LEENKX_MT_splash:
+ original_splash_cls = cls
+ bpy.utils.unregister_class(cls)
+ bpy.utils.register_class(LEENKX_MT_splash)
+
+
+def unregister_splash_menu():
+ global original_splash_cls
+ bpy.utils.unregister_class(LEENKX_MT_splash)
+ if original_splash_cls is not None:
+ bpy.utils.register_class(original_splash_cls)
+ original_splash_cls = None
+
+
+saved_draws = {}
+
+
+def override_draw(bl_idname, new_draw):
+ cls = getattr(bpy.types, bl_idname, None)
+ if cls is None:
+ print("Leenkx menu: class not found", bl_idname)
+ return
+ if bl_idname not in saved_draws:
+ saved_draws[bl_idname] = cls.draw
+ elif cls.draw is new_draw:
+ return
+ cls.draw = new_draw
+
+
+def restore_draw(bl_idname):
+ cls = getattr(bpy.types, bl_idname, None)
+ if cls is not None and bl_idname in saved_draws:
+ cls.draw = saved_draws[bl_idname]
+
+def editor_menus_draw(self, context):
+ layout = self.layout
+ if logo_icon_id:
+ layout.menu("TOPBAR_MT_blender", text="", icon_value=logo_icon_id)
+ else:
+ layout.menu("TOPBAR_MT_blender", text="Leenkx")
+ layout.menu("TOPBAR_MT_file")
+ layout.menu("TOPBAR_MT_edit")
+ layout.menu("TOPBAR_MT_render")
+ layout.menu("TOPBAR_MT_window")
+ layout.menu("TOPBAR_MT_help")
+
+
+def blender_draw(self, _context):
+ layout = self.layout
+ layout.operator("wm.splash", text="Leenkx Splash")
+ layout.separator()
+ layout.operator("preferences.app_template_install",
+ text="Install Application Template...")
+ layout.separator()
+ layout.menu("TOPBAR_MT_blender_system")
+
+
+def help_draw(self, context):
+ layout = self.layout
+ show_developer = context.preferences.view.show_developer_ui
+
+ layout.operator("wm.url_open", text="Leenkx Docs",
+ icon='URL').url = "https://leenkx.com/api"
+ layout.operator("wm.url_open", text="Leenkx Support",
+ icon='URL').url = "https://leenkx.com/contact"
+ layout.operator("wm.url_open", text="Tutorials",
+ icon='URL').url = "https://leenkx.com/tutorials"
+ layout.operator("wm.url_open", text="Community",
+ icon='URL').url = "https://leenkx.com/community"
+ layout.operator("wm.url_open", text="Support Leenkx (Donate)",
+ icon='FUND').url = "https://leenkx.com/donate"
+
+ layout.separator()
+
+ if show_developer:
+ layout.operator("wm.url_open", text="Developer Docs",
+ icon='URL').url = "https://leenkx.com/wiki"
+ layout.operator("wm.url_open_preset",
+ text="Python API Reference").type = 'API'
+
+ layout.separator()
+
+ layout.operator("wm.url_open", text="Report a Bug",
+ icon='URL').url = "https://leenkx.com/contact"
+ layout.operator("wm.sysinfo")
+
+
+def register(sdk_path: str):
+ install_theme(sdk_path)
+ setup_splash(sdk_path)
+ load_icons()
+ register_splash_menu()
+ override_draw("TOPBAR_MT_editor_menus", editor_menus_draw)
+ override_draw("TOPBAR_MT_blender", blender_draw)
+ override_draw("TOPBAR_MT_help", help_draw)
+
+
+def unregister():
+ restore_draw("TOPBAR_MT_editor_menus")
+ restore_draw("TOPBAR_MT_blender")
+ restore_draw("TOPBAR_MT_help")
+ saved_draws.clear()
+ unregister_splash_menu()
+
+ global preview_collection
+ if preview_collection is not None:
+ bpy.utils.previews.remove(preview_collection)
+ preview_collection = None
+ os.environ.pop("BLENDER_CUSTOM_SPLASH", None)
+
+ window = bpy.context.window_manager.windows[0]
+ if bpy.app.version >= (3, 0, 0):
+ with bpy.context.temp_override(window=window):
+ bpy.ops.preferences.reset_default_theme()
+ else:
+ override = bpy.context.copy()
+ override['window'] = window
+ bpy.ops.preferences.reset_default_theme(override_context=override)