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)