intellij-plugin/features/ide-onboarding/src/com/jetbrains/edu/uiOnboarding/steps/ActionGroupZhabaStep.kt (177 lines of code) (raw):
package com.jetbrains.edu.uiOnboarding.steps
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.actionSystem.ActionManager
import com.intellij.openapi.actionSystem.ActionToolbar
import com.intellij.openapi.actionSystem.toolbarLayout.ToolbarLayoutStrategy
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.popup.Balloon
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.openapi.util.NlsContexts
import com.intellij.openapi.util.text.HtmlChunk
import com.intellij.ui.ColorUtil
import com.intellij.ui.DrawUtil
import com.intellij.ui.GotItComponentBuilder.Companion.getArrowShift
import com.intellij.ui.JBColor
import com.intellij.ui.RemoteTransferUIManager
import com.intellij.ui.awt.RelativePoint
import com.intellij.ui.components.JBLabel
import com.intellij.util.ui.GridBag
import com.intellij.util.ui.JBUI
import com.jetbrains.edu.uiOnboarding.EduUiOnboardingAnimation
import com.jetbrains.edu.uiOnboarding.EduUiOnboardingAnimationData
import com.jetbrains.edu.uiOnboarding.EduUiOnboardingAnimationData.Companion.EYE_SHIFT
import com.jetbrains.edu.uiOnboarding.EduUiOnboardingAnimationData.Companion.SMALL_SHIFT
import com.jetbrains.edu.uiOnboarding.EduUiOnboardingAnimationStep
import com.jetbrains.edu.uiOnboarding.ZhabaComponent
import com.jetbrains.edu.uiOnboarding.actions.ZHABA_SAYS_ACTION_PLACE
import com.jetbrains.edu.uiOnboarding.stepsGraph.ActionGroupZhabaData
import com.jetbrains.edu.uiOnboarding.stepsGraph.GraphData
import com.jetbrains.edu.uiOnboarding.stepsGraph.ZhabaStep
import com.jetbrains.edu.uiOnboarding.stepsGraph.ZhabaStep.Companion.FINISH_TRANSITION
import kotlinx.coroutines.CoroutineScope
import java.awt.*
import javax.swing.JComponent
import javax.swing.JPanel
import javax.swing.border.EmptyBorder
/**
* This step is displayed as a ballon with a
* - [title]
* - vertically layered buttons for actions of the [actionGroup]
*
* This step generates transitions only if it is interrupted.
* The idea is that it will be interrupted by calling some action from the [actionGroup].
*/
class ActionGroupZhabaStep(
override val stepId: String,
private val actionGroup: ActionGroup,
@NlsContexts.NotificationTitle private val title: String
) : ZhabaStep<ActionGroupZhabaData, GraphData.EMPTY> {
private fun buildAnimation(data: EduUiOnboardingAnimationData, point: RelativePoint): EduUiOnboardingAnimation =
object : EduUiOnboardingAnimation {
override val steps: List<EduUiOnboardingAnimationStep> = listOfNotNull(
EduUiOnboardingAnimationStep(data.lookRight, point, point, 2_000),
EduUiOnboardingAnimationStep(data.lookLeft, point, point, 1_000),
)
override val cycle: Boolean = true
}
private val useContrastColors = false
override fun performStep(
project: Project,
data: EduUiOnboardingAnimationData
): ActionGroupZhabaData? {
val relativeZhabaPoint = locateZhabaInProjectToolWindow(project) ?: return null
val zhabaPoint = relativeZhabaPoint.originalPoint
val component = relativeZhabaPoint.originalComponent
val zhabaComponent = ZhabaComponent(project)
zhabaComponent.animation = buildAnimation(data, relativeZhabaPoint)
// Position the balloon at the bottom of the project view component
val tooltipPoint = Point(zhabaPoint.x + EYE_SHIFT, zhabaPoint.y - SMALL_SHIFT)
val tooltipRelativePoint = RelativePoint(component, tooltipPoint)
// Copied and simplified from com.intellij.ui.GotItComponentBuilder.createContent.
val builder = JBPopupFactory.getInstance()
.createBalloonBuilder(createContent())
.setHideOnAction(false)
.setHideOnClickOutside(false)
.setHideOnFrameResize(false)
.setHideOnKeyOutside(false)
.setHideOnClickOutside(false)
.setBlockClicksThroughBalloon(true)
.setRequestFocus(false)
.setBorderColor(getBorderColor())
.setCornerToPointerDistance(getArrowShift())
.setFillColor(JBUI.CurrentTheme.GotItTooltip.background(false))
.setPointerSize(JBUI.size(16, 8))
.setCornerRadius(JBUI.CurrentTheme.GotItTooltip.CORNER_RADIUS.get())
return ActionGroupZhabaData(builder, tooltipRelativePoint, Balloon.Position.above, relativeZhabaPoint, zhabaComponent)
}
override suspend fun executeStep(
stepData: ActionGroupZhabaData,
graphData: GraphData.EMPTY,
cs: CoroutineScope,
disposable: Disposable
): String {
val builder = stepData.builder
builder.setCloseButtonEnabled(false)
builder.setDisposable(disposable)
val showInCenter = stepData.position == null
val balloon = builder.createBalloon()
balloon.setAnimationEnabled(false)
if (showInCenter) {
balloon.showInCenterOf(stepData.tooltipPoint.originalComponent as JComponent)
}
else {
balloon.show(stepData.tooltipPoint, stepData.position)
}
return stepData.zhaba.start(cs) ?: FINISH_TRANSITION
}
private fun getBorderColor(): Color {
val borderColor = JBUI.CurrentTheme.GotItTooltip.borderColor(useContrastColors)
val simpleBorderColor = JBColor.namedColor("GotItTooltip.borderSimplifiedColor", borderColor)
return JBColor.lazy { if (DrawUtil.isSimplifiedUI()) simpleBorderColor else borderColor }
}
private fun createContent(): JComponent {
val actionToolbar = createActionToolbar()
// Everything that follows is copied and simplified from com.intellij.ui.GotItComponentBuilder.createContent.
// We suppose that there is no icon and no step index in the ballon, also no buttons at the bottom.
// The message text is substituted with the action toolbar.
val panel = JPanel(GridBagLayout())
val gc = GridBag()
val left = 0
val column = 0
if (title.isNotEmpty()) {
gc.nextLine()
val finalText = HtmlChunk.raw(title)
.bold()
.wrapWith(HtmlChunk.font(ColorUtil.toHtmlColor(JBUI.CurrentTheme.GotItTooltip.headerForeground(useContrastColors))))
.wrapWith(HtmlChunk.html())
.toString()
val constraints = gc.setColumn(column).anchor(GridBagConstraints.LINE_START).insets(1, left, 0, 0)
panel.add(JBLabel(finalText), constraints)
}
gc.nextLine()
val constraints = gc
.setColumn(column)
.anchor(GridBagConstraints.LINE_START)
.insets(if (title.isNotEmpty()) JBUI.CurrentTheme.GotItTooltip.TEXT_INSET.get() else 0, left, 0, 0)
panel.add(actionToolbar.component, constraints)
panel.background = JBUI.CurrentTheme.GotItTooltip.background(useContrastColors)
panel.border = EmptyBorder(JBUI.CurrentTheme.GotItTooltip.insets())
RemoteTransferUIManager.forceDirectTransfer(panel)
return panel
}
private fun createActionToolbar(): ActionToolbar {
val actionToolbar = ActionManager.getInstance().createActionToolbar(ZHABA_SAYS_ACTION_PLACE, actionGroup, false)
// We need gaps between buttons. It is possible to set up the Toolbar if the components for actions are ActionButton, but
// we use plain JButtons, so we need to add gaps some other way.
// Here we delegate the layout to the existing layout strategy but modify its layout adding gaps over each button.
actionToolbar.layoutStrategy = addGapsInLayoutStrategy(
actionToolbar.layoutStrategy,
JBUI.scale(6),
JBUI.scale(12)
)
actionToolbar.setMiniMode(false)
actionToolbar.component.background = JBUI.CurrentTheme.GotItTooltip.background(useContrastColors)
actionToolbar.component.border = JBUI.Borders.empty()
return actionToolbar
}
// We suppose here that all buttons have primary padding, except the last one, which has secondary padding.
private fun addGapsInLayoutStrategy(
existingLayoutStrategy: ToolbarLayoutStrategy,
primaryPadding: Int,
secondaryPadding: Int
): ToolbarLayoutStrategy = object : ToolbarLayoutStrategy {
override fun calculateBounds(toolbar: ActionToolbar): List<Rectangle?>? {
val result = existingLayoutStrategy.calculateBounds(toolbar) ?: return null
return result.mapIndexed { index, rectangle ->
val y = if (index < result.lastIndex) {
(2 * index + 1) * primaryPadding
}
else {
2 * index * primaryPadding + secondaryPadding
}
Rectangle(
rectangle.x,
rectangle.y + y,
rectangle.width,
rectangle.height
)
}
}
override fun calcPreferredSize(toolbar: ActionToolbar): Dimension? {
val actions = toolbar.actions.size
val result = existingLayoutStrategy.calcPreferredSize(toolbar) ?: return null
return Dimension(result.width, result.height + addY(actions) )
}
override fun calcMinimumSize(toolbar: ActionToolbar): Dimension? {
val actions = toolbar.actions.size
val result = existingLayoutStrategy.calcMinimumSize(toolbar) ?: return null
return Dimension(result.width, result.height + addY(actions))
}
private fun addY(actions: Int) =
(actions - 1) * 2 * primaryPadding + 2 * secondaryPadding
}
}