diff --git a/platform/core-impl/src/com/intellij/openapi/application/impl/ApplicationInfoImpl.java b/platform/core-impl/src/com/intellij/openapi/application/impl/ApplicationInfoImpl.java
index cb3ee401aa7a..2d5f82f9c019 100644
--- a/platform/core-impl/src/com/intellij/openapi/application/impl/ApplicationInfoImpl.java
+++ b/platform/core-impl/src/com/intellij/openapi/application/impl/ApplicationInfoImpl.java
@@ -7,6 +7,7 @@ import com.intellij.diagnostic.StartUpMeasurer;
import com.intellij.ide.plugins.PluginManagerCore;
import com.intellij.openapi.application.ApplicationNamesInfo;
import com.intellij.openapi.application.IdeUrlTrackingParametersProvider;
+import com.intellij.openapi.application.PathManager;
import com.intellij.openapi.application.ex.ApplicationInfoEx;
import com.intellij.openapi.application.ex.ApplicationManagerEx;
import com.intellij.openapi.diagnostic.Logger;
@@ -20,6 +21,8 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.VisibleForTesting;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.text.MessageFormat;
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
@@ -34,6 +37,7 @@ import java.util.*;
public final class ApplicationInfoImpl extends ApplicationInfoEx {
public static final String DEFAULT_PLUGINS_HOST = "https://plugins.jetbrains.com";
public static final String IDEA_PLUGINS_HOST_PROPERTY = "idea.plugins.host";
+ public static final String FREE_MODE_SPLASH_MARKER_FILE_NAME = "splash-free-mode.txt";
private static final String IDEA_APPLICATION_INFO_DEFAULT_DARK_LAF = "idea.application.info.default.dark.laf";
private static final String IDEA_APPLICATION_INFO_DEFAULT_CLASSIC_DARK_LAF = "idea.application.info.default.classic.dark.laf";
@@ -57,6 +61,7 @@ public final class ApplicationInfoImpl extends ApplicationInfoEx {
private String myShortCompanyName;
private String myCompanyUrl = "https://www.jetbrains.com/";
private @Nullable String splashImageUrl;
+ private @Nullable String freeModeSplashImageUrl;
private @Nullable String eapSplashImageUrl;
private String svgIconUrl;
private String mySvgEapIconUrl;
@@ -144,6 +149,11 @@ public final class ApplicationInfoImpl extends ApplicationInfoEx {
}
break;
+ case "logo-free-mode": {
+ freeModeSplashImageUrl = getAttributeValue(child, "url");
+ }
+ break;
+
case "icon": {
svgIconUrl = child.getAttributeValue("svg");
mySmallSvgIconUrl = child.getAttributeValue("svg-small");
@@ -432,7 +442,16 @@ public final class ApplicationInfoImpl extends ApplicationInfoEx {
@Override
public @Nullable String getSplashImageUrl() {
- return isEap && eapSplashImageUrl != null ? eapSplashImageUrl : splashImageUrl;
+ if (isEap && eapSplashImageUrl != null) return eapSplashImageUrl;
+
+ if (freeModeSplashImageUrl != null) {
+ Path markerFile = PathManager.getConfigDir().resolve(FREE_MODE_SPLASH_MARKER_FILE_NAME);
+ if (Files.exists(markerFile)) {
+ return freeModeSplashImageUrl;
+ }
+ }
+
+ return splashImageUrl;
}
@Override
diff --git a/platform/platform-resources/src/idea/ApplicationInfo.xsd b/platform/platform-resources/src/idea/ApplicationInfo.xsd
index 6874204898a0..a4cbfba02d07 100644
--- a/platform/platform-resources/src/idea/ApplicationInfo.xsd
+++ b/platform/platform-resources/src/idea/ApplicationInfo.xsd
@@ -9,6 +9,7 @@
+
diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/application/impl/ApplicationInfoTest.java b/platform/platform-tests/testSrc/com/intellij/openapi/application/impl/ApplicationInfoTest.java
index 520fd20cf5ad..5eecfdbcf4fd 100644
--- a/platform/platform-tests/testSrc/com/intellij/openapi/application/impl/ApplicationInfoTest.java
+++ b/platform/platform-tests/testSrc/com/intellij/openapi/application/impl/ApplicationInfoTest.java
@@ -1,15 +1,20 @@
-// Copyright 2000-2023 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
+// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
package com.intellij.openapi.application.impl;
+import com.intellij.openapi.application.ApplicationInfo;
import com.intellij.testFramework.PlatformTestUtil;
import com.intellij.util.xml.dom.XmlElement;
import org.jetbrains.annotations.NotNull;
import org.junit.Test;
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
+import static com.intellij.openapi.application.impl.UtilKt.withTempConfigDirectory;
import static org.assertj.core.api.Assertions.assertThat;
public class ApplicationInfoTest {
@@ -31,10 +36,114 @@ public class ApplicationInfoTest {
});
}
+ @Test
+ public void splashImage_eapLogoPreferredInEap() {
+ ApplicationInfo info = createAppInfo(eap(true), logo, logoEap);
+ assertThat(info.isEAP()).isTrue();
+ String url = info.getSplashImageUrl();
+ assertThat(url).isEqualTo(logoEapUrl);
+ }
+
+ @Test
+ public void splashImage_regularLogoWhenNotEap() {
+ ApplicationInfo info = createAppInfo(eap(false), logo, logoEap);
+ assertThat(info.isEAP()).isFalse();
+ String url = info.getSplashImageUrl();
+ assertThat(url).isEqualTo(logoUrl);
+ }
+
+ @Test
+ public void splashImage_regularLogoInEapWithNoEapLogoProvided() {
+ ApplicationInfo info = createAppInfo(eap(true), logo);
+ assertThat(info.isEAP()).isTrue();
+ String url = info.getSplashImageUrl();
+ assertThat(url).isEqualTo(logoUrl);
+ }
+
+ @Test
+ public void splashImage_freeModeLogoPreferredWhenMarkerPresent() {
+ withTempConfigDirectory(configDir -> {
+ createFreeModeSplashMarkerFile(configDir);
+ ApplicationInfo info = createAppInfo(logo, logoFreeMode);
+ String url = info.getSplashImageUrl();
+ assertThat(url).isEqualTo(logoFreeModeUrl);
+ });
+ }
+
+ @Test
+ public void splashImage_regularLogoWhenNoFreeModeMarker() {
+ withTempConfigDirectory(configDir -> {
+ deleteFreeModeSplashMarkerFile(configDir);
+ ApplicationInfo info = createAppInfo(logo, logoFreeMode);
+ String url = info.getSplashImageUrl();
+ assertThat(url).isEqualTo(logoUrl);
+ });
+ }
+
+ @Test
+ public void splashImage_regularLogoWhenMarkerPresentButNoFreeModeLogoProvided() {
+ withTempConfigDirectory(configDir -> {
+ createFreeModeSplashMarkerFile(configDir);
+ ApplicationInfo info = createAppInfo(logo);
+ String url = info.getSplashImageUrl();
+ assertThat(url).isEqualTo(logoUrl);
+ });
+ }
+
+ @Test
+ public void splashImage_eapLogoBeatsFreeModeInEapWhenBothAvailableAndMarkerPresent() {
+ withTempConfigDirectory(configDir -> {
+ createFreeModeSplashMarkerFile(configDir);
+ ApplicationInfo info = createAppInfo(eap(true), logo, logoEap, logoFreeMode);
+ String url = info.getSplashImageUrl();
+ assertThat(url).isEqualTo(logoEapUrl);
+ });
+ }
+
+ @Test
+ public void splashImage_freeModeLogoWhenNotEapAndMarkerPresent() {
+ withTempConfigDirectory(configDir -> {
+ createFreeModeSplashMarkerFile(configDir);
+ ApplicationInfo info = createAppInfo(eap(false), logo, logoEap, logoFreeMode);
+ String url = info.getSplashImageUrl();
+ assertThat(url).isEqualTo(logoFreeModeUrl);
+ });
+ }
+
+ @Test
+ public void splashImage_regularLogoWhenNotEapAndNoMarker() {
+ withTempConfigDirectory(configDir -> {
+ deleteFreeModeSplashMarkerFile(configDir);
+ ApplicationInfo info = createAppInfo(eap(false), logo, logoEap, logoFreeMode);
+ String url = info.getSplashImageUrl();
+ assertThat(url).isEqualTo(logoUrl);
+ });
+ }
+
public static @NotNull ApplicationInfoImpl createAppInfo(@NotNull XmlElement @NotNull ... content) {
var children = new ArrayList<>(List.of(content));
children.add(new XmlElement("icon", Map.of("svg", "xxx.svg", "svg-small", "xxx.svg"), List.of(), null));
children.add(new XmlElement("plugins", Map.of(), List.of(), null));
return new ApplicationInfoImpl(new XmlElement("state", Map.of(), children, null));
}
+
+ private static void createFreeModeSplashMarkerFile(@NotNull Path configDir) throws IOException {
+ Files.writeString(configDir.resolve(ApplicationInfoImpl.FREE_MODE_SPLASH_MARKER_FILE_NAME), "");
+ }
+
+ private static void deleteFreeModeSplashMarkerFile(@NotNull Path configDir) throws IOException {
+ Files.deleteIfExists(configDir.resolve(ApplicationInfoImpl.FREE_MODE_SPLASH_MARKER_FILE_NAME));
+ }
+
+ private static XmlElement eap(boolean isEap) {
+ return new XmlElement("version", Map.of("eap", String.valueOf(isEap)), List.of(), null);
+ }
+
+ private static final String logoUrl = "/logo.png";
+ private static final String logoEapUrl = "/logo_eap.png";
+ private static final String logoFreeModeUrl = "/logo_free.png";
+
+ private final XmlElement logo = new XmlElement("logo", Map.of("url", logoUrl), List.of(), null);
+ private final XmlElement logoEap = new XmlElement("logo-eap", Map.of("url", logoEapUrl), List.of(), null);
+ private final XmlElement logoFreeMode = new XmlElement("logo-free-mode", Map.of("url", logoFreeModeUrl), List.of(), null);
}
diff --git a/platform/platform-tests/testSrc/com/intellij/openapi/application/impl/util.kt b/platform/platform-tests/testSrc/com/intellij/openapi/application/impl/util.kt
index c070db3365b9..6f5889aaa417 100644
--- a/platform/platform-tests/testSrc/com/intellij/openapi/application/impl/util.kt
+++ b/platform/platform-tests/testSrc/com/intellij/openapi/application/impl/util.kt
@@ -2,19 +2,19 @@
package com.intellij.openapi.application.impl
import com.intellij.concurrency.ContextAwareRunnable
-import com.intellij.openapi.application.Application
-import com.intellij.openapi.application.EDT
-import com.intellij.openapi.application.ModalityState
-import com.intellij.openapi.application.asContextElement
+import com.intellij.openapi.application.*
import com.intellij.testFramework.LeakHunter
+import com.intellij.testFramework.utils.io.deleteRecursively
import com.intellij.util.ui.EDT
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withContext
-import org.junit.jupiter.api.Assertions.assertNotNull
+import org.junit.jupiter.api.function.ThrowingConsumer
+import java.nio.file.Path
import java.util.function.Supplier
import javax.swing.SwingUtilities
import kotlin.coroutines.resume
+import kotlin.io.path.createTempDirectory
import kotlin.test.assertFalse
fun Application.withModality(action: () -> Unit) {
@@ -64,3 +64,26 @@ suspend fun pumpEDT() {
internal suspend fun processApplicationQueue() {
withContext(Dispatchers.EDT + ModalityState.any().asContextElement()) {}
}
+
+internal fun withTempDirectory(block: ThrowingConsumer) {
+ val tempDirectory = createTempDirectory()
+ try {
+ block.accept(tempDirectory)
+ }
+ finally {
+ tempDirectory.deleteRecursively()
+ }
+}
+
+internal fun withTempConfigDirectory(block: ThrowingConsumer) {
+ withTempDirectory { tempDirectory ->
+ val oldConfigDir = PathManager.getConfigDir()
+ try {
+ PathManager.setExplicitConfigPath(tempDirectory)
+ block.accept(tempDirectory)
+ }
+ finally {
+ PathManager.setExplicitConfigPath(oldConfigDir);
+ }
+ }
+}