diff --git a/core/os/os.cpp b/core/os/os.cpp index 716126d232c..ef4669a5eab 100644 --- a/core/os/os.cpp +++ b/core/os/os.cpp @@ -309,11 +309,16 @@ String OS::get_bundle_resource_dir() const { return "."; } -// Path to macOS .app bundle embedded icon +// Path to macOS .app bundle embedded icon (.icns file). String OS::get_bundle_icon_path() const { return String(); } +// Name of macOS .app bundle embedded icon (Liquid Glass asset name). +String OS::get_bundle_icon_name() const { + return String(); +} + // OS specific path for user:// String OS::get_user_data_dir(const String &p_user_dir) const { return "."; diff --git a/core/os/os.h b/core/os/os.h index bbd1571738e..a0026461212 100644 --- a/core/os/os.h +++ b/core/os/os.h @@ -294,6 +294,7 @@ public: virtual String get_temp_path() const; virtual String get_bundle_resource_dir() const; virtual String get_bundle_icon_path() const; + virtual String get_bundle_icon_name() const; virtual String get_user_data_dir(const String &p_user_dir) const; virtual String get_user_data_dir() const; diff --git a/editor/inspector/editor_property_name_processor.cpp b/editor/inspector/editor_property_name_processor.cpp index e94aff2deab..25ab6b5edee 100644 --- a/editor/inspector/editor_property_name_processor.cpp +++ b/editor/inspector/editor_property_name_processor.cpp @@ -148,6 +148,7 @@ EditorPropertyNameProcessor::EditorPropertyNameProcessor() { capitalize_string_remaps["aa"] = "AA"; capitalize_string_remaps["aabb"] = "AABB"; capitalize_string_remaps["adb"] = "ADB"; + capitalize_string_remaps["actool"] = "actool"; capitalize_string_remaps["ao"] = "AO"; capitalize_string_remaps["api"] = "API"; capitalize_string_remaps["apk"] = "APK"; diff --git a/main/main.cpp b/main/main.cpp index b6bf1a2646e..552fb68a159 100644 --- a/main/main.cpp +++ b/main/main.cpp @@ -4533,8 +4533,13 @@ int Main::start() { sml->add_current_scene(scene); #ifdef MACOS_ENABLED +#ifndef TOOLS_ENABLED + if ((FileAccess::exists(OS::get_singleton()->get_bundle_resource_dir().path_join("Assets.car")) && !OS::get_singleton()->get_bundle_icon_name().is_empty()) || (!OS::get_singleton()->get_bundle_icon_path().is_empty())) { + has_icon = true; // Bundle has embedded icon, do not override with project icon. + } +#endif String mac_icon_path = GLOBAL_GET("application/config/macos_native_icon"); - if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_NATIVE_ICON) && !mac_icon_path.is_empty()) { + if (DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_NATIVE_ICON) && !mac_icon_path.is_empty() && !has_icon) { DisplayServer::get_singleton()->set_native_icon(mac_icon_path); has_icon = true; } diff --git a/misc/dist/macos_template.app/Contents/Info.plist b/misc/dist/macos_template.app/Contents/Info.plist index 708498dd9c4..e48a5ff1d24 100644 --- a/misc/dist/macos_template.app/Contents/Info.plist +++ b/misc/dist/macos_template.app/Contents/Info.plist @@ -12,6 +12,7 @@ $name CFBundleIconFile icon.icns +$liquid_glass_icon CFBundleIdentifier $bundle_identifier CFBundleInfoDictionaryVersion diff --git a/platform/macos/doc_classes/EditorExportPlatformMacOS.xml b/platform/macos/doc_classes/EditorExportPlatformMacOS.xml index c6ffed2522b..82687686138 100644 --- a/platform/macos/doc_classes/EditorExportPlatformMacOS.xml +++ b/platform/macos/doc_classes/EditorExportPlatformMacOS.xml @@ -38,6 +38,11 @@ Interpolation method used to resize application icon. + + macOS 26 Liquid Glass icon source file. Use [url=https://developer.apple.com/icon-composer/]Icon Composer[/url] to create Liquid Glass icons. + [b]Note:[/b] Supported when exporting from macOS only, Xcode 26+ required. + [b]Note:[/b] Liquid Glass icons are supported on macOS 26 only, use [member application/icon] to set the icon for older macOS versions. + Minimum version of macOS required for this application to run on Apple Silicon Macs, in the [code]major.minor.patch[/code] or [code]major.minor[/code] format, can only contain numeric characters ([code]0-9[/code]) and periods ([code].[/code]). diff --git a/platform/macos/export/export.cpp b/platform/macos/export/export.cpp index 0c7018d2a8f..707d66bfc4e 100644 --- a/platform/macos/export/export.cpp +++ b/platform/macos/export/export.cpp @@ -45,6 +45,8 @@ void register_macos_exporter() { #else EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/macos/rcodesign", PROPERTY_HINT_GLOBAL_FILE)); #endif + EDITOR_DEF_BASIC("export/macos/actool", ""); + EditorSettings::get_singleton()->add_property_hint(PropertyInfo(Variant::STRING, "export/macos/actool", PROPERTY_HINT_GLOBAL_FILE)); #endif Ref platform; diff --git a/platform/macos/export/export_plugin.cpp b/platform/macos/export/export_plugin.cpp index 60e339756a9..0521639c0d5 100644 --- a/platform/macos/export/export_plugin.cpp +++ b/platform/macos/export/export_plugin.cpp @@ -306,6 +306,12 @@ bool EditorExportPlatformMacOS::get_export_option_visibility(const EditorExportP return false; } +#ifndef MACOS_ENABLED + if (p_option == "application/liquid_glass_icon") { + return false; + } +#endif + String custom_prof = p_preset->get("codesign/entitlements/custom_file"); if (!custom_prof.is_empty() && p_option != "codesign/entitlements/custom_file" && p_option.begins_with("codesign/entitlements/")) { return false; @@ -467,6 +473,7 @@ void EditorExportPlatformMacOS::get_export_options(List *r_options r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "custom_template/release", PROPERTY_HINT_GLOBAL_FILE, "*.zip"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "debug/export_console_wrapper", PROPERTY_HINT_ENUM, "No,Debug Only,Debug and Release"), 1)); + r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/liquid_glass_icon", PROPERTY_HINT_FILE, "*.icon"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/icon", PROPERTY_HINT_FILE, "*.icns,*.png,*.webp,*.svg"), "")); r_options->push_back(ExportOption(PropertyInfo(Variant::INT, "application/icon_interpolation", PROPERTY_HINT_ENUM, "Nearest neighbor,Bilinear,Cubic,Trilinear,Lanczos"), 4)); r_options->push_back(ExportOption(PropertyInfo(Variant::STRING, "application/bundle_identifier", PROPERTY_HINT_PLACEHOLDER_TEXT, "com.example.game"), "", false, true)); @@ -828,7 +835,7 @@ void EditorExportPlatformMacOS::_fix_privacy_manifest(const Ref &p_preset, Vector &plist, const String &p_binary) { +void EditorExportPlatformMacOS::_fix_plist(const Ref &p_preset, Vector &plist, const String &p_binary, bool p_lg_icon_exported, const String &p_lg_icon) { String str = String::utf8((const char *)plist.ptr(), plist.size()); String strnew; Vector lines = str.split("\n"); @@ -872,6 +879,12 @@ void EditorExportPlatformMacOS::_fix_plist(const Ref &p_pres strnew += lines[i].replace("$xcodever", p_preset->get("xcode/xcode_version")) + "\n"; } else if (lines[i].contains("$xcodebuild")) { strnew += lines[i].replace("$xcodebuild", p_preset->get("xcode/xcode_build")) + "\n"; + } else if (lines[i].contains("$liquid_glass_icon")) { + if (p_lg_icon_exported) { + strnew += lines[i].replace("$liquid_glass_icon", "\tCFBundleIconName\n\t" + p_lg_icon + "\n"); + } else { + strnew += lines[i].replace("$liquid_glass_icon", ""); + } } else if (lines[i].contains("$usage_descriptions")) { String descriptions; if (!((String)p_preset->get("privacy/microphone_usage_description")).is_empty()) { @@ -933,6 +946,76 @@ void EditorExportPlatformMacOS::_fix_plist(const Ref &p_pres } } +Error EditorExportPlatformMacOS::_export_liquid_glass_icon(const Ref &p_preset, const String &p_app_path, const String &p_icon_path) { + String actool = EDITOR_GET("export/macos/actool").operator String(); + if (actool.is_empty()) { + actool = "actool"; + } + + List args; + args.push_back("--version"); + String str; + String err_str; + int exitcode = 0; + + Error err = OS::get_singleton()->execute(actool, args, &str, &exitcode, true); + if (err != OK) { + add_message(EXPORT_MESSAGE_WARNING, TTR("Liquid Glass Icons"), TTR("Could not start 'actool' executable.")); + return err; + } + PList info_plist; + if (!info_plist.load_string(str, err_str)) { + print_verbose(str); + add_message(EXPORT_MESSAGE_WARNING, TTR("Liquid Glass Icons"), TTR("Could not read 'actool' version.")); + return err; + } + if (info_plist.get_root()->data_type == PList::PLNodeType::PL_NODE_TYPE_DICT && info_plist.get_root()->data_dict.has("com.apple.actool.version")) { + Ref dict = info_plist.get_root()->data_dict["com.apple.actool.version"]; + if (dict->data_type == PList::PLNodeType::PL_NODE_TYPE_DICT && dict->data_dict.has("short-bundle-version")) { + float version = String::utf8(dict->data_dict["short-bundle-version"]->data_string.get_data()).to_float(); + if (version < 26.0) { + add_message(EXPORT_MESSAGE_WARNING, TTR("Liquid Glass Icons"), vformat(TTR("At least version 26.0 of 'actool' is required (version %f found)."), version)); + return ERR_UNAVAILABLE; + } + } + } + str.clear(); + + String plist = EditorPaths::get_singleton()->get_temp_dir().path_join("assetcatalog.plist"); + args.clear(); + args.push_back(ProjectSettings::get_singleton()->globalize_path(p_icon_path)); + args.push_back("--compile"); + args.push_back(p_app_path + "/Contents/Resources/"); + args.push_back("--output-format"); + args.push_back("human-readable-text"); + args.push_back("--lightweight-asset-runtime-mode"); + args.push_back("enabled"); + args.push_back("--app-icon"); + args.push_back(p_icon_path.get_file().get_basename()); + args.push_back("--include-all-app-icons"); + args.push_back("--enable-on-demand-resources"); + args.push_back("NO"); + args.push_back("--development-region"); + args.push_back("en"); + args.push_back("--target-device"); + args.push_back("mac"); + args.push_back("--minimum-deployment-target"); + args.push_back("26"); + args.push_back("--platform"); + args.push_back("macosx"); + args.push_back("--output-partial-info-plist"); + args.push_back(plist); + + err = OS::get_singleton()->execute(actool, args, &str, &exitcode, true); + if (err != OK || str.contains("error:") || !FileAccess::exists(p_app_path + "/Contents/Resources/Assets.car") || !FileAccess::exists(plist)) { + print_verbose(str); + add_message(EXPORT_MESSAGE_WARNING, TTR("Liquid Glass Icons"), TTR("Could not export liquid glass icon:") + "\n" + str); + return err; + } + + return OK; +} + /** * If we're running the macOS version of the Godot editor we'll: * - export our application bundle to a temporary folder @@ -1861,7 +1944,16 @@ Error EditorExportPlatformMacOS::export_project(const Ref &p } if (file == "Contents/Info.plist") { - _fix_plist(p_preset, data, pkg_name); + bool lg_icon_expored = false; + String lg_icon = p_preset->get("application/liquid_glass_icon"); +#ifdef MACOS_ENABLED + // Export liquid glass. + if (!lg_icon.is_empty()) { + lg_icon_expored = (_export_liquid_glass_icon(p_preset, tmp_app_path_name, lg_icon) == OK); + } +#endif + // Modify plist. + _fix_plist(p_preset, data, pkg_name, lg_icon_expored, lg_icon.get_file().get_basename()); } if (file == "Contents/Resources/PrivacyInfo.xcprivacy") { diff --git a/platform/macos/export/export_plugin.h b/platform/macos/export/export_plugin.h index cabcc865462..4eea8dc1906 100644 --- a/platform/macos/export/export_plugin.h +++ b/platform/macos/export/export_plugin.h @@ -85,9 +85,10 @@ class EditorExportPlatformMacOS : public EditorExportPlatform { int menu_options = 0; void _fix_privacy_manifest(const Ref &p_preset, Vector &plist); - void _fix_plist(const Ref &p_preset, Vector &plist, const String &p_binary); + void _fix_plist(const Ref &p_preset, Vector &plist, const String &p_binary, bool p_lg_icon_exported, const String &p_lg_icon); void _make_icon(const Ref &p_preset, const Ref &p_icon, Vector &p_data); + Error _export_liquid_glass_icon(const Ref &p_preset, const String &p_app_path, const String &p_icon_path); Error _notarize(const Ref &p_preset, const String &p_path); void _code_sign(const Ref &p_preset, const String &p_path, const String &p_ent_path, bool p_warn = true, bool p_set_id = false); void _code_sign_directory(const Ref &p_preset, const String &p_path, const String &p_ent_path, const String &p_helper_ent_path, bool p_should_error_on_non_code = true); diff --git a/platform/macos/os_macos.h b/platform/macos/os_macos.h index 8cf47902b1d..f1c12c8df42 100644 --- a/platform/macos/os_macos.h +++ b/platform/macos/os_macos.h @@ -103,6 +103,7 @@ public: virtual String get_temp_path() const override; virtual String get_bundle_resource_dir() const override; virtual String get_bundle_icon_path() const override; + virtual String get_bundle_icon_name() const override; virtual String get_godot_dir_name() const override; virtual String get_system_dir(SystemDir p_dir, bool p_shared_storage = true) const override; diff --git a/platform/macos/os_macos.mm b/platform/macos/os_macos.mm index 6a6eae96878..7f75896935c 100644 --- a/platform/macos/os_macos.mm +++ b/platform/macos/os_macos.mm @@ -469,6 +469,19 @@ String OS_MacOS::get_bundle_icon_path() const { return ret; } +String OS_MacOS::get_bundle_icon_name() const { + String ret; + + NSBundle *main = [NSBundle mainBundle]; + if (main) { + NSString *icon_name = [[main infoDictionary] objectForKey:@"CFBundleIconName"]; + if (icon_name) { + ret.append_utf8([icon_name UTF8String]); + } + } + return ret; +} + // Get properly capitalized engine name for system paths String OS_MacOS::get_godot_dir_name() const { return String(GODOT_VERSION_SHORT_NAME).capitalize();