IDEA-328256 Replace New UI Onboarding dialog banner with animation

WebM video is used here instead of suitable vector Lottie animation, because animation content is 3D, and it is unable to be rendered by vector graphics.

GitOrigin-RevId: 72dddbdfcd7f332f59faf9f9121e28b4dd631a05
This commit is contained in:
Konstantin Hudyakov
2023-08-13 11:33:27 +03:00
committed by intellij-monorepo-bot
parent 19e37ec9f7
commit f2823a7d20
6 changed files with 88 additions and 28 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 KiB

View File

@@ -4,16 +4,19 @@ package com.intellij.platform.ide.newUiOnboarding
import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.DialogWrapper
import com.intellij.openapi.util.IconLoader
import com.intellij.openapi.util.text.HtmlChunk
import com.intellij.ui.*
import com.intellij.ui.dsl.builder.panel
import com.intellij.ui.dsl.gridLayout.UnscaledGaps
import com.intellij.ui.dsl.gridLayout.UnscaledGapsY
import com.intellij.ui.jcef.JBCefBrowser
import com.intellij.ui.scale.JBUIScale
import com.intellij.util.ui.JBDimension
import com.intellij.util.ui.JBFont
import com.intellij.util.ui.JBUI
import com.intellij.util.ui.UIUtil
import java.awt.Font
import java.util.*
import javax.swing.JComponent
import javax.swing.JRootPane
import javax.swing.border.Border
@@ -29,15 +32,21 @@ class NewUiOnboardingDialog(project: Project)
}
override fun createCenterPanel(): JComponent {
// todo: replace with separate banner when it will be ready
val banner = IconLoader.getIcon("newUiOnboarding/banner.png", NewUiOnboardingDialog::class.java.classLoader)
val videoSize = JBDimension(384, 242)
val contentGaps = UnscaledGaps(28, 32, 22, 32)
val panel = panel {
row {
icon(banner)
val videoBase64 = readVideoAsBase64()
val browser = JBCefBrowser.createBuilder().setMouseWheelEventEnable(false).build()
val pageHtml = createVideoHtmlPage(videoBase64)
browser.loadHTML(pageHtml)
cell(browser.component)
.customize(UnscaledGaps.EMPTY)
.applyToComponent { WindowMoveListener(this).installTo(this) }
.applyToComponent {
WindowMoveListener(this).installTo(components?.firstOrNull() ?: this)
preferredSize = videoSize
}
}
panel {
row {
@@ -48,7 +57,7 @@ class NewUiOnboardingDialog(project: Project)
}
}
row {
val maxWidth = banner.iconWidth - JBUI.scale(contentGaps.width)
val maxWidth = videoSize.width - JBUI.scale(contentGaps.width)
val charWidth = window.getFontMetrics(JBFont.label()).charWidth('0')
val maxLineLength = maxWidth / charWidth
text(NewUiOnboardingBundle.message("dialog.text"), maxLineLength)
@@ -78,4 +87,35 @@ class NewUiOnboardingDialog(project: Project)
}
override fun createContentPaneBorder(): Border? = null
private fun createVideoHtmlPage(videoBase64: String): String {
val componentId = "video"
val head = HtmlChunk.head().child(LottieUtils.getSingleContentCssStyles(Gray._32, componentId))
val videoTag = HtmlChunk.tag("video")
.attr("id", componentId)
.attr("autoplay")
.attr("loop")
.attr("muted")
.child(HtmlChunk.tag("source")
.attr("type", "video/webm")
.attr("src", "data:video/webm;base64,$videoBase64"))
val body = HtmlChunk.body().child(videoTag)
return HtmlChunk.html()
.child(head)
.child(body)
.toString()
}
private fun readVideoAsBase64(): String {
val url = NewUiOnboardingDialog::class.java.classLoader.getResource(VIDEO_PATH)
?: error("Failed to find file by path: $VIDEO_PATH")
val videoBytes = url.readBytes()
return Base64.getEncoder().encodeToString(videoBytes)
}
companion object {
private const val VIDEO_PATH: String = "newUiOnboarding/DialogVideo.webm"
}
}

View File

@@ -16,26 +16,10 @@ import java.awt.Dimension
object LottieUtils {
@Suppress("HardCodedStringLiteral")
fun createLottieAnimationPage(lottieJson: String, lottieScript: String? = null, background: Color): String {
val head = HtmlBuilder().append(
HtmlChunk.tag("style").addRaw("""
body {
background-color: #${ColorUtil.toHex(background)};
margin: 0;
height: 100%;
overflow: hidden;
}
#lottie {
background-color: #${ColorUtil.toHex(background)};
width: 100%;
height: 100%;
display: block;
overflow: hidden;
transform: translate3d(0,0,0);
text-align: center;
opacity: 1;
}
""".trimIndent())
).wrapWith(HtmlChunk.head())
val componentId = "lottie"
val head = HtmlBuilder()
.append(getSingleContentCssStyles(background, componentId))
.wrapWith(HtmlChunk.head())
val script = if (lottieScript != null) {
HtmlChunk.tag("script").addRaw(lottieScript)
@@ -44,7 +28,7 @@ object LottieUtils {
val body = HtmlBuilder()
.append(script)
.append(HtmlChunk.div().attr("id", "lottie").addRaw(""))
.append(HtmlChunk.div().attr("id", componentId).addRaw(""))
.append(HtmlChunk.tag("script").addRaw("""
const animationData = $lottieJson;
const params = {
@@ -65,6 +49,28 @@ object LottieUtils {
.toString()
}
@Suppress("HardCodedStringLiteral")
fun getSingleContentCssStyles(background: Color, componentId: String): HtmlChunk {
return HtmlChunk.tag("style").addRaw("""
body {
background-color: #${ColorUtil.toHex(background)};
margin: 0;
height: 100%;
overflow: hidden;
}
#${componentId} {
background-color: #${ColorUtil.toHex(background)};
width: 100%;
height: 100%;
display: block;
overflow: hidden;
transform: translate3d(0,0,0);
text-align: center;
opacity: 1;
}
""".trimIndent())
}
@Throws(SerializationException::class)
fun getLottieImageSize(lottieJson: String): Dimension {
val json = Json { ignoreUnknownKeys = true }

View File

@@ -125,7 +125,10 @@ public abstract class HtmlChunk {
public void appendTo(@NotNull StringBuilder builder) {
builder.append('<').append(myTagName);
myAttributes.entrySet().stream().sorted(Map.Entry.comparingByKey()).forEach(entry -> {
builder.append(' ').append(entry.getKey()).append("=\"").append(StringUtil.escapeXmlEntities(entry.getValue())).append('"');
builder.append(' ').append(entry.getKey());
if (entry.getValue() != null) {
builder.append("=\"").append(StringUtil.escapeXmlEntities(entry.getValue())).append('"');
}
});
if (myChildren.isEmpty()) {
builder.append("/>");
@@ -158,6 +161,17 @@ public abstract class HtmlChunk {
return new Element(myTagName, myAttributes.with(name, Integer.toString(value)), myChildren);
}
/**
* Adds an attribute without '=' sign and a value
*
* @param name attribute name
* @return a new element that is like this element but has the specified attribute added or replaced
*/
@Contract(pure = true)
public @NotNull Element attr(@NonNls String name) {
return new Element(myTagName, myAttributes.with(name, null), myChildren);
}
/**
* @param style CSS style specification
* @return a new element that is like this element but has the specified style added or replaced