IJPL-248623 Use platform dropdown link for prompt launch settings

The inline launch-settings selector previously painted its own trailing arrow, which split the visual state from platform hit testing. Reuse DropDownLink for the text and chevron, and keep the provider icon as a small sibling label that delegates to the same popup action.

This preserves the compact composer tray layout while matching the existing combo-like link behavior. Cover the icon-click path in the palette structure test and update the behavior spec.

GitOrigin-RevId: f3b37c5869f23e06c495d5b9f84e32205c5d0b08
This commit is contained in:
Vladimir Krivosheev
2026-07-09 18:59:27 +00:00
committed by intellij-monorepo-bot
parent 202f9bf836
commit f3e1850d2c
3 changed files with 78 additions and 50 deletions
@@ -28,6 +28,7 @@ import com.intellij.openapi.project.DumbAwareToggleAction
import com.intellij.ui.EditorTextField
import com.intellij.ui.WindowMoveListener
import com.intellij.ui.components.ActionLink
import com.intellij.ui.components.DropDownLink
import com.intellij.ui.components.JBLabel
import com.intellij.ui.components.JBList
import com.intellij.ui.components.JBScrollPane
@@ -52,7 +53,6 @@ import java.awt.Container
import java.awt.Cursor
import java.awt.Dimension
import java.awt.FlowLayout
import java.awt.Graphics
import java.awt.event.ContainerAdapter
import java.awt.event.ContainerEvent
import java.awt.event.MouseAdapter
@@ -317,12 +317,6 @@ internal class AgentPromptToolbarIconToggleAction(
internal class HeaderActionLink(text: @Nls String) : ActionLink(text) {
var onVisibilityChanged: (() -> Unit)? = null
var onPresentationChanged: (() -> Unit)? = null
var trailingIcon: Icon? = null
set(value) {
field = value
revalidate()
repaint()
}
override fun setVisible(aFlag: Boolean) {
val visibilityChanged = isVisible != aFlag
@@ -339,32 +333,14 @@ internal class HeaderActionLink(text: @Nls String) : ActionLink(text) {
onPresentationChanged?.invoke()
}
}
}
override fun getPreferredSize(): Dimension {
return super.getPreferredSize().withTrailingIconWidth()
}
override fun getMinimumSize(): Dimension {
return super.getMinimumSize().withTrailingIconWidth()
}
override fun paintComponent(g: Graphics) {
super.paintComponent(g)
val icon = trailingIcon ?: return
val x = if (componentOrientation.isLeftToRight) {
width - insets.right - icon.iconWidth
}
else {
insets.left
}
val y = (height - icon.iconHeight) / 2
icon.paintIcon(this, g, x, y)
}
private fun Dimension.withTrailingIconWidth(): Dimension {
val icon = trailingIcon ?: return this
width += icon.iconWidth + iconTextGap
return this
internal class HeaderDropDownLink(
text: @Nls String,
private val onPerform: (HeaderDropDownLink) -> Unit,
) : DropDownLink<String>(text, { error("HeaderDropDownLink handles popup display in performAction") }) {
override fun performAction() {
onPerform(this)
}
}
@@ -383,23 +359,42 @@ internal class AgentPromptToolbarProfileAction(
var onPresentationChanged: (() -> Unit)? = null
@JvmField
val link: HeaderActionLink = HeaderActionLink(initialText).apply {
val link: HeaderDropDownLink = HeaderDropDownLink(initialText) { source ->
popupHandler.invoke(DataManager.getInstance().getDataContext(source), source)
}.apply {
autoHideOnDisable = false
withFont(JBUI.Fonts.smallFont().asPlain())
foreground = UIUtil.getContextHelpForeground()
border = JBUI.Borders.empty()
setIcon(initialIcon, false)
trailingIcon = AllIcons.General.LinkDropTriangle
setToolTipText(HtmlChunk.text(initialDescription))
accessibleContext.accessibleName = initialText
accessibleContext.accessibleDescription = initialDescription
addActionListener {
popupHandler.invoke(DataManager.getInstance().getDataContext(this), this)
}
}
val customComponent: JComponent
get() = link
@JvmField
val iconLabel: JLabel = JLabel(initialIcon).apply {
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
border = JBUI.Borders.emptyRight(4)
labelFor = link
setToolTipText(HtmlChunk.text(initialDescription))
accessibleContext.accessibleName = initialText
accessibleContext.accessibleDescription = initialDescription
addMouseListener(object : MouseAdapter() {
override fun mousePressed(e: MouseEvent) {
if (e.button == MouseEvent.BUTTON1 && link.isEnabled) {
link.doClick()
}
}
})
}
private val component = JPanel(FlowLayout(FlowLayout.LEFT, 0, 0)).apply {
isOpaque = false
add(iconLabel)
add(link)
}
val customComponent: JComponent = component
val textForTest: @Nls String
get() = profileText
@@ -430,19 +425,28 @@ internal class AgentPromptToolbarProfileAction(
templatePresentation.description = description
templatePresentation.icon = icon
link.text = text
component.isVisible = visible
component.isEnabled = enabled
link.isVisible = visible
link.isEnabled = enabled
link.setIcon(icon, false)
iconLabel.icon = icon
iconLabel.isVisible = visible
iconLabel.isEnabled = enabled
link.setToolTipText(HtmlChunk.text(description))
link.accessibleContext.accessibleName = text
link.accessibleContext.accessibleDescription = description
iconLabel.setToolTipText(HtmlChunk.text(description))
iconLabel.accessibleContext.accessibleName = text
iconLabel.accessibleContext.accessibleDescription = description
component.revalidate()
component.repaint()
link.revalidate()
link.repaint()
onPresentationChanged?.invoke()
}
override fun createCustomComponent(presentation: Presentation, place: String): JComponent {
return link
return component
}
override fun update(e: AnActionEvent) {
@@ -710,7 +714,7 @@ internal fun createAgentPromptPaletteView(
}
val generationSettingsActionsPanel = JPanel(FlowLayout(FlowLayout.RIGHT, 0, 0)).apply {
isOpaque = false
add(launchProfileLink)
add(profileSelectorAction.customComponent)
defaultProfileActionControl.component.border = JBUI.Borders.emptyLeft(if (isInlinePrompt) 6 else 8)
add(defaultProfileActionControl.component)
}
@@ -460,8 +460,10 @@ class AgentPromptPaletteViewStructureTest {
val promptAreaInRoot = checkNotNull(findPromptArea(view.rootPanel, promptArea))
val profileActionComponent = view.profileAction.customComponent
assertThat(profileActionComponent).isSameAs(view.launchProfileLink)
assertThat(profileActionComponent).isNotSameAs(view.launchProfileLink)
assertThat(SwingUtilities.isDescendingFrom(view.launchProfileLink, view.rootPanel)).isTrue()
assertThat(SwingUtilities.isDescendingFrom(view.launchProfileLink, profileActionComponent)).isTrue()
assertThat(SwingUtilities.isDescendingFrom(view.profileAction.iconLabel, profileActionComponent)).isTrue()
assertThat(SwingUtilities.isDescendingFrom(profileActionComponent, view.rootPanel)).isTrue()
assertThat(SwingUtilities.isDescendingFrom(profileActionComponent, view.rightHeaderPanel)).isFalse()
assertThat(SwingUtilities.isDescendingFrom(profileActionComponent, view.headerControls.toolbarComponent)).isFalse()
@@ -500,14 +502,14 @@ class AgentPromptPaletteViewStructureTest {
assertThat(view.generationSettingsPanel.isVisible).isTrue()
assertThat(view.launchProfileLink.text).isEqualTo("Default")
assertThat(view.profileAction.textForTest).isEqualTo("Default")
assertThat(view.launchProfileLink.icon).isNotNull()
assertThat(view.profileAction.iconLabel.icon).isNotNull()
assertThat(view.launchProfileLink.icon).isSameAs(view.addContextButton.icon)
assertThat(view.launchProfileLink.font.isBold).isFalse()
assertThat((view.launchProfileLink as HeaderActionLink).trailingIcon).isSameAs(view.addContextButton.icon)
assertThat(view.launchTuningSummaryLink.isVisible).isFalse()
val launchTuningSummaryCenter = SwingUtilities.convertPoint(
view.launchProfileLink,
view.launchProfileLink.width / 2,
view.launchProfileLink.height / 2,
profileActionComponent,
profileActionComponent.width / 2,
profileActionComponent.height / 2,
view.rootPanel,
)
val topComponent = SwingUtilities.getDeepestComponentAt(view.rootPanel, launchTuningSummaryCenter.x, launchTuningSummaryCenter.y)
@@ -515,6 +517,28 @@ class AgentPromptPaletteViewStructureTest {
}
}
@Test
fun launchSettingsProviderIconOpensSameControlAsDropdownLink() {
runInEdtAndWait {
val view = createAgentPromptPaletteView(
promptArea = EditorTextField(),
contextChipsPanel = JPanel(),
onExistingTaskSelected = {},
)
var popupAnchor: JComponent? = null
view.profileAction.setPopupHandler { _, anchor -> popupAnchor = anchor }
triggerMousePressed(view.profileAction.iconLabel)
assertThat(popupAnchor).isSameAs(view.launchProfileLink)
popupAnchor = null
view.launchProfileLink.doClick()
assertThat(popupAnchor).isSameAs(view.launchProfileLink)
}
}
@Test
fun productionPromptFieldDoesNotReserveOverlaySpaceForGenerationSettingsControls() {
runInEdtAndWait {
@@ -36,7 +36,7 @@ Task-cost profiles make the global prompt ready to send while still letting user
- This spec does not require renaming the existing persisted `activeLaunchProfileId` field; it may remain the stored default profile id for compatibility.
## Requirements
- The prompt composer exposes the launch-settings affordance in the bottom tray location specified by `global-prompt-composer.spec.md`. The affordance must show the selected provider icon, including the red YOLO/Brave badge for YOLO profiles, plus the compact selected profile name or generated profile name, and open one popup for profile choice, per-task tuning, and profile management. Default/save/update actions stay visible inline next to the launch-settings affordance. Built-in provider-backed profiles use compact mode labels, such as `Default`, `Full Auto`, `Skip Permissions`, or `Brave Mode`, to avoid repeating provider names. User profiles show their saved profile name, and unmatched edited controls show a generated compact name, such as `High` or `GPT-5 High`.
- The prompt composer exposes the launch-settings affordance in the bottom tray location specified by `global-prompt-composer.spec.md`. The affordance must show the selected provider icon, including the red YOLO/Brave badge for YOLO profiles, plus the compact selected profile name or generated profile name, and clicking either the icon or the text/dropdown link opens one popup for profile choice, per-task tuning, and profile management. Default/save/update actions stay visible inline next to the launch-settings affordance. Built-in provider-backed profiles use compact mode labels, such as `Default`, `Full Auto`, `Skip Permissions`, or `Brave Mode`, to avoid repeating provider names. User profiles show their saved profile name, and unmatched edited controls show a generated compact name, such as `High` or `GPT-5 High`.
[@test] ../../prompt/ui/testSrc/AgentPromptPaletteViewStructureTest.kt
- The prompt must restore the stored default profile on open when that profile is still applicable. If the default is unavailable, the prompt may fall back to the provider-list default without writing a replacement default.