mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-21 05:52:35 +08:00
fix(jetbrains): refine migration wizard layout
This commit is contained in:
+10
-4
@@ -3,15 +3,18 @@ package ai.kilocode.client.migration.ui
|
||||
import ai.kilocode.client.migration.MigrationUiSelections
|
||||
import ai.kilocode.client.migration.MigrationUiState
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.layout.HAlign
|
||||
import ai.kilocode.client.ui.layout.VAlign
|
||||
import ai.kilocode.client.ui.layout.align
|
||||
import com.intellij.ui.components.JBPanel
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import java.awt.BorderLayout
|
||||
import javax.swing.JComponent
|
||||
|
||||
/**
|
||||
* Outer container for the migration wizard rendered inside the blocker layer.
|
||||
*
|
||||
* Wraps [MigrationWizardPanel] in a bordered overlay with a panel background.
|
||||
* Wraps [MigrationWizardPanel] in the blocker layer.
|
||||
* Build once; call [update] on every state change.
|
||||
*/
|
||||
class MigrationOverlayPanel : JBPanel<MigrationOverlayPanel>(BorderLayout()) {
|
||||
@@ -40,13 +43,16 @@ class MigrationOverlayPanel : JBPanel<MigrationOverlayPanel>(BorderLayout()) {
|
||||
|
||||
init {
|
||||
withBackground(UiStyle.Colors.bg())
|
||||
border = JBUI.Borders.customLine(com.intellij.ui.JBColor.border(), 1)
|
||||
add(wizard, BorderLayout.CENTER)
|
||||
add(wizard.align(HAlign.CENTER, VAlign.CENTER), BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun update(state: MigrationUiState.Needed) {
|
||||
wizard.update(state)
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun preferredFocusComponent(): JComponent = wizard.preferredFocusComponent()
|
||||
}
|
||||
|
||||
+75
-161
@@ -9,34 +9,27 @@ import ai.kilocode.client.migration.MigrationUiState
|
||||
import ai.kilocode.client.migration.SessionMigrationSummary
|
||||
import ai.kilocode.client.migration.groupStatus
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.views.base.BaseQuestionView
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.layout.Stack
|
||||
import ai.kilocode.rpc.dto.LegacyMigrationDetectionDto
|
||||
import ai.kilocode.rpc.dto.LegacyMigrationSessionProgressDto
|
||||
import ai.kilocode.rpc.dto.MigrationItemCategoryDto
|
||||
import ai.kilocode.rpc.dto.MigrationItemProgressStatusDto
|
||||
import ai.kilocode.rpc.dto.MigrationSessionPhaseDto
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ide.BrowserUtil
|
||||
import com.intellij.ui.components.JBCheckBox
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import com.intellij.util.ui.JBFont
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.components.BorderLayoutPanel
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.CardLayout
|
||||
import java.awt.FlowLayout
|
||||
import java.awt.GridBagConstraints
|
||||
import java.awt.GridBagLayout
|
||||
import javax.swing.JButton
|
||||
import java.awt.Component
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.JButton as JBtn
|
||||
|
||||
private const val CARD_WHATS_NEW = "whats-new"
|
||||
private const val CARD_MIGRATE = "migrate"
|
||||
private const val ACTION_SKIP = "skip"
|
||||
private const val ACTION_MIGRATE = "migrate"
|
||||
private const val ACTION_DONE = "done"
|
||||
private const val ACTION_CONTINUE = "continue"
|
||||
|
||||
/**
|
||||
* Two-screen migration wizard: "What's New" → "Migrate Your Settings".
|
||||
* Migration selection wizard.
|
||||
*
|
||||
* Build once; call [update] for every state change. Does not rebuild the component tree.
|
||||
*/
|
||||
@@ -49,13 +42,6 @@ class MigrationWizardPanel : JPanel(BorderLayout()) {
|
||||
var onDone: (() -> Unit)? = null
|
||||
var onContinueFromError: (() -> Unit)? = null
|
||||
|
||||
// ------ Card layout ------
|
||||
private val cards = CardLayout()
|
||||
private val cardPanel = JPanel(cards)
|
||||
|
||||
// ------ What's New screen ------
|
||||
private val whatsNewPanel = buildWhatsNewPanel()
|
||||
|
||||
// ------ Migrate screen row state ------
|
||||
private val rows = mutableMapOf<MigrationItemCategoryDto, MigrationItemRow>()
|
||||
private val settingsRow = MigrationItemRow(KiloBundle.message("migration.row.settings"), MigrationItemCategoryDto.settings)
|
||||
@@ -67,12 +53,8 @@ class MigrationWizardPanel : JPanel(BorderLayout()) {
|
||||
|
||||
private val sessionProgress = SessionMigrationProgressPanel()
|
||||
private val sessionSummary = SessionMigrationSummaryPanel()
|
||||
|
||||
private val migrateBtn = JButton(KiloBundle.message("migration.button.migrate"))
|
||||
private val backBtn = JButton(KiloBundle.message("migration.button.back"))
|
||||
private val skipBtn = JButton(KiloBundle.message("migration.button.skip"))
|
||||
private val doneBtn = JButton(KiloBundle.message("migration.button.done"))
|
||||
private val continueBtn = JButton(KiloBundle.message("migration.button.continue"))
|
||||
private val question = BaseQuestionView()
|
||||
private val keepBox = JBCheckBox(KiloBundle.message("migration.keep_legacy_settings"), true)
|
||||
|
||||
private val emptyLabel = JBLabel(KiloBundle.message("migration.empty")).apply {
|
||||
foreground = UiStyle.Colors.weak()
|
||||
@@ -80,6 +62,8 @@ class MigrationWizardPanel : JPanel(BorderLayout()) {
|
||||
|
||||
private var detection: LegacyMigrationDetectionDto? = null
|
||||
private var selections = MigrationUiSelections()
|
||||
private var phase = MigrationUiPhase.selecting
|
||||
private var running = false
|
||||
|
||||
init {
|
||||
isOpaque = false
|
||||
@@ -95,25 +79,38 @@ class MigrationWizardPanel : JPanel(BorderLayout()) {
|
||||
row.onSelectionChanged = { _ -> updateMigrateButtonEnabled() }
|
||||
}
|
||||
|
||||
migrateBtn.addActionListener { onStart?.invoke(currentSelections()) }
|
||||
backBtn.addActionListener { cards.show(cardPanel, CARD_WHATS_NEW) }
|
||||
skipBtn.addActionListener { onSkip?.invoke() }
|
||||
doneBtn.addActionListener { onDone?.invoke() }
|
||||
continueBtn.addActionListener { onContinueFromError?.invoke() }
|
||||
question.setHeader(
|
||||
KiloBundle.message("migration.migrate.title"),
|
||||
KiloBundle.message("migration.migrate.subtitle"),
|
||||
)
|
||||
question.setContent(buildContent())
|
||||
question.setActions(
|
||||
listOf(
|
||||
BaseQuestionView.Action(ACTION_SKIP, KiloBundle.message("migration.button.skip"), primary = false) {
|
||||
onSkip?.invoke()
|
||||
},
|
||||
BaseQuestionView.Action(ACTION_MIGRATE, KiloBundle.message("migration.button.migrate"), primary = true) {
|
||||
onStart?.invoke(currentSelections())
|
||||
},
|
||||
BaseQuestionView.Action(ACTION_DONE, KiloBundle.message("migration.button.done"), primary = true) {
|
||||
onDone?.invoke()
|
||||
},
|
||||
BaseQuestionView.Action(ACTION_CONTINUE, KiloBundle.message("migration.button.continue"), primary = true) {
|
||||
onContinueFromError?.invoke()
|
||||
},
|
||||
),
|
||||
)
|
||||
question.setActionLeft(keepBox)
|
||||
|
||||
sessionSummary.onForceReimport = { ids -> onForce?.invoke(ids) }
|
||||
|
||||
cardPanel.isOpaque = false
|
||||
cardPanel.add(whatsNewPanel, CARD_WHATS_NEW)
|
||||
cardPanel.add(buildMigratePanel(), CARD_MIGRATE)
|
||||
|
||||
add(cardPanel, BorderLayout.CENTER)
|
||||
|
||||
cards.show(cardPanel, CARD_WHATS_NEW)
|
||||
add(question, BorderLayout.CENTER)
|
||||
updateButtons(MigrationUiPhase.selecting, running = false)
|
||||
}
|
||||
|
||||
// ------ Public update ------
|
||||
|
||||
@RequiresEdt
|
||||
fun update(state: MigrationUiState.Needed) {
|
||||
val det = state.detection
|
||||
// Detection and default selections are set once on first update and are stable for the
|
||||
@@ -125,7 +122,8 @@ class MigrationWizardPanel : JPanel(BorderLayout()) {
|
||||
applyDefaults(det)
|
||||
}
|
||||
|
||||
val phase = state.phase
|
||||
phase = state.phase
|
||||
running = state.running
|
||||
|
||||
// Update row visibility based on what data exists
|
||||
providerRow.isVisible = det.providers.any { it.supported }
|
||||
@@ -164,11 +162,18 @@ class MigrationWizardPanel : JPanel(BorderLayout()) {
|
||||
sessionSummary.isVisible = false
|
||||
}
|
||||
|
||||
updateButtons(phase, state.running)
|
||||
updateButtons(phase, running)
|
||||
updateMigrateButtonEnabled()
|
||||
question.revalidate()
|
||||
question.repaint()
|
||||
revalidate()
|
||||
repaint()
|
||||
}
|
||||
|
||||
fun preferredFocusComponent() = migrateBtn
|
||||
@RequiresEdt
|
||||
fun preferredFocusComponent(): JComponent = question.actionButtonsForTest()[ACTION_MIGRATE] ?: question
|
||||
|
||||
internal fun keepLegacySettingsFileSelectedForTest() = keepBox.isSelected
|
||||
|
||||
// ------ Internal helpers ------
|
||||
|
||||
@@ -187,6 +192,7 @@ class MigrationWizardPanel : JPanel(BorderLayout()) {
|
||||
defaults.settings.autoApproval.taskPermission ||
|
||||
defaults.settings.language ||
|
||||
defaults.settings.autocomplete
|
||||
keepBox.isSelected = defaults.keepLegacySettingsFile
|
||||
}
|
||||
|
||||
private fun updateRowProgress(category: MigrationItemCategoryDto, items: List<MigrationItemUiProgress>) {
|
||||
@@ -201,22 +207,24 @@ class MigrationWizardPanel : JPanel(BorderLayout()) {
|
||||
}
|
||||
|
||||
private fun updateButtons(phase: MigrationUiPhase, running: Boolean) {
|
||||
backBtn.isVisible = phase == MigrationUiPhase.selecting
|
||||
skipBtn.isVisible = phase == MigrationUiPhase.selecting
|
||||
migrateBtn.isVisible = phase == MigrationUiPhase.selecting || phase == MigrationUiPhase.migrating
|
||||
migrateBtn.isEnabled = !running && phase == MigrationUiPhase.selecting
|
||||
migrateBtn.text = if (running) KiloBundle.message("migration.button.migrating") else KiloBundle.message("migration.button.migrate")
|
||||
doneBtn.isVisible = phase == MigrationUiPhase.done
|
||||
continueBtn.isVisible = phase == MigrationUiPhase.error
|
||||
question.setActionVisible(ACTION_SKIP, phase == MigrationUiPhase.selecting)
|
||||
question.setActionVisible(ACTION_MIGRATE, phase == MigrationUiPhase.selecting || phase == MigrationUiPhase.migrating)
|
||||
question.setActionText(
|
||||
ACTION_MIGRATE,
|
||||
if (running) KiloBundle.message("migration.button.migrating") else KiloBundle.message("migration.button.migrate"),
|
||||
)
|
||||
question.setActionVisible(ACTION_DONE, phase == MigrationUiPhase.done)
|
||||
question.setActionVisible(ACTION_CONTINUE, phase == MigrationUiPhase.error)
|
||||
keepBox.isVisible = phase == MigrationUiPhase.selecting
|
||||
}
|
||||
|
||||
private fun updateMigrateButtonEnabled() {
|
||||
val any = rows.values.any { it.isVisible && it.selected }
|
||||
migrateBtn.isEnabled = any && migrateBtn.text == KiloBundle.message("migration.button.migrate")
|
||||
question.setActionEnabled(ACTION_MIGRATE, any && phase == MigrationUiPhase.selecting && !running)
|
||||
}
|
||||
|
||||
private fun currentSelections(): MigrationUiSelections {
|
||||
val det = detection ?: return MigrationUiSelections()
|
||||
val det = detection ?: return MigrationUiSelections(keepLegacySettingsFile = keepBox.isSelected)
|
||||
val providers = if (providerRow.selected) det.providers.filter { it.supported && it.hasApiKey }.map { it.profileName } else emptyList()
|
||||
val mcpServers = if (mcpRow.selected) det.mcpServers.map { it.name } else emptyList()
|
||||
val modes = if (modesRow.selected) det.customModes.map { it.slug } else emptyList()
|
||||
@@ -229,116 +237,22 @@ class MigrationWizardPanel : JPanel(BorderLayout()) {
|
||||
sessions = sessions,
|
||||
defaultModel = modelRow.selected,
|
||||
settings = if (settingsRow.selected) defaults.settings else MigrationSettingsUiSelections(),
|
||||
keepLegacySettingsFile = keepBox.isSelected,
|
||||
)
|
||||
}
|
||||
|
||||
private fun buildWhatsNewPanel(): JPanel {
|
||||
val panel = JPanel(BorderLayout()).apply { isOpaque = false }
|
||||
|
||||
val title = JBLabel(KiloBundle.message("migration.whats_new.title")).apply {
|
||||
font = JBFont.h2().asBold()
|
||||
border = JBUI.Borders.emptyBottom(UiStyle.Gap.sm())
|
||||
private fun buildContent(): JComponent {
|
||||
return Stack.vertical(gap = UiStyle.Gap.xs()).apply {
|
||||
alignmentX = Component.LEFT_ALIGNMENT
|
||||
}
|
||||
val subtitle = JBLabel(KiloBundle.message("migration.whats_new.subtitle")).apply {
|
||||
foreground = UiStyle.Colors.weak()
|
||||
border = JBUI.Borders.emptyBottom(UiStyle.Gap.md())
|
||||
}
|
||||
|
||||
val features = listOf(
|
||||
KiloBundle.message("migration.whats_new.feature.performance"),
|
||||
KiloBundle.message("migration.whats_new.feature.interface"),
|
||||
KiloBundle.message("migration.whats_new.feature.agent_manager"),
|
||||
KiloBundle.message("migration.whats_new.feature.foundation"),
|
||||
)
|
||||
|
||||
val featurePanel = JPanel(GridBagLayout()).apply { isOpaque = false }
|
||||
val gc = GridBagConstraints().apply {
|
||||
fill = GridBagConstraints.HORIZONTAL
|
||||
weightx = 1.0
|
||||
gridx = 0
|
||||
}
|
||||
for (f in features) {
|
||||
val row = JPanel(FlowLayout(FlowLayout.LEFT, UiStyle.Gap.xs(), 0)).apply {
|
||||
isOpaque = false
|
||||
add(JBLabel(AllIcons.General.InspectionsOK))
|
||||
add(JBLabel(f))
|
||||
}
|
||||
featurePanel.add(row, gc)
|
||||
}
|
||||
|
||||
val content = JPanel(GridBagLayout()).apply {
|
||||
isOpaque = false
|
||||
border = JBUI.Borders.empty(UiStyle.Gap.pad())
|
||||
val c = GridBagConstraints().apply { fill = GridBagConstraints.HORIZONTAL; weightx = 1.0; gridx = 0 }
|
||||
add(title, c)
|
||||
add(subtitle, c)
|
||||
add(featurePanel, c)
|
||||
}
|
||||
|
||||
val continueWnBtn = JButton(KiloBundle.message("migration.button.continue_to_migrate")).apply {
|
||||
addActionListener { cards.show(cardPanel, CARD_MIGRATE) }
|
||||
}
|
||||
val footer = JPanel(FlowLayout(FlowLayout.RIGHT, UiStyle.Gap.sm(), 0)).apply {
|
||||
isOpaque = false
|
||||
add(continueWnBtn)
|
||||
}
|
||||
|
||||
panel.add(JBScrollPane(content).apply { border = JBUI.Borders.empty() }, BorderLayout.CENTER)
|
||||
panel.add(footer, BorderLayout.SOUTH)
|
||||
return panel
|
||||
}
|
||||
|
||||
private fun buildMigratePanel(): JPanel {
|
||||
val panel = JPanel(BorderLayout()).apply { isOpaque = false }
|
||||
|
||||
val title = JBLabel(KiloBundle.message("migration.migrate.title")).apply {
|
||||
font = JBFont.h2().asBold()
|
||||
border = JBUI.Borders.emptyBottom(UiStyle.Gap.sm())
|
||||
}
|
||||
val subtitle = JBLabel(KiloBundle.message("migration.migrate.subtitle")).apply {
|
||||
foreground = UiStyle.Colors.weak()
|
||||
border = JBUI.Borders.emptyBottom(UiStyle.Gap.md())
|
||||
}
|
||||
val sectionLabel = JBLabel(KiloBundle.message("migration.migrate.section")).apply {
|
||||
font = JBFont.medium()
|
||||
border = JBUI.Borders.emptyBottom(UiStyle.Gap.xs())
|
||||
}
|
||||
|
||||
val rowsPanel = JPanel(GridBagLayout()).apply {
|
||||
isOpaque = false
|
||||
val gc = GridBagConstraints().apply { fill = GridBagConstraints.HORIZONTAL; weightx = 1.0; gridx = 0 }
|
||||
add(emptyLabel, gc)
|
||||
add(providerRow, gc)
|
||||
add(mcpRow, gc)
|
||||
add(modesRow, gc)
|
||||
add(sessionsRow, gc)
|
||||
add(modelRow, gc)
|
||||
add(settingsRow, gc)
|
||||
add(sessionProgress, gc)
|
||||
add(sessionSummary, gc)
|
||||
}
|
||||
|
||||
val content = JPanel(GridBagLayout()).apply {
|
||||
isOpaque = false
|
||||
border = JBUI.Borders.empty(UiStyle.Gap.pad())
|
||||
val gc = GridBagConstraints().apply { fill = GridBagConstraints.HORIZONTAL; weightx = 1.0; gridx = 0 }
|
||||
add(title, gc)
|
||||
add(subtitle, gc)
|
||||
add(sectionLabel, gc)
|
||||
add(rowsPanel, gc)
|
||||
}
|
||||
|
||||
val footer = JPanel(FlowLayout(FlowLayout.RIGHT, UiStyle.Gap.sm(), 0)).apply {
|
||||
isOpaque = false
|
||||
add(backBtn)
|
||||
add(skipBtn)
|
||||
add(migrateBtn)
|
||||
add(doneBtn)
|
||||
add(continueBtn)
|
||||
}
|
||||
|
||||
panel.add(JBScrollPane(content).apply { border = JBUI.Borders.empty() }, BorderLayout.CENTER)
|
||||
panel.add(footer, BorderLayout.SOUTH)
|
||||
return panel
|
||||
.next(emptyLabel)
|
||||
.next(providerRow)
|
||||
.next(mcpRow)
|
||||
.next(modesRow)
|
||||
.next(sessionsRow)
|
||||
.next(modelRow)
|
||||
.next(settingsRow)
|
||||
.next(sessionProgress)
|
||||
.next(sessionSummary)
|
||||
}
|
||||
}
|
||||
|
||||
+30
-11
@@ -5,7 +5,10 @@ import ai.kilocode.client.session.ui.style.SessionEditorStyleTarget
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.ui.RoundedContentPanel
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.layout.HAlign
|
||||
import ai.kilocode.client.ui.layout.Stack
|
||||
import ai.kilocode.client.ui.layout.VAlign
|
||||
import ai.kilocode.client.ui.layout.align
|
||||
import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.ui.components.JBTextArea
|
||||
@@ -84,6 +87,8 @@ class BaseQuestionView : RoundedContentPanel(
|
||||
isVisible = false
|
||||
}
|
||||
|
||||
private val iconWrap = icon.align(HAlign.LEFT, VAlign.TOP)
|
||||
|
||||
private val headerText: JBTextArea = makeText("", UiStyle.Colors.fg(), bold = true)
|
||||
private val descriptionText: JBTextArea = makeText("", UiStyle.Colors.weak(), bold = false)
|
||||
|
||||
@@ -95,14 +100,15 @@ class BaseQuestionView : RoundedContentPanel(
|
||||
private val actionButtons = mutableMapOf<String, JButton>()
|
||||
private val actionOrder = mutableListOf<String>()
|
||||
|
||||
private val actions = Stack.horizontal(gap = UiStyle.Gap.sm())
|
||||
private val mainActions = Stack.horizontal(gap = UiStyle.Gap.sm())
|
||||
|
||||
private val sideActions = Stack.horizontal()
|
||||
|
||||
private val footer = JPanel(BorderLayout()).apply {
|
||||
isOpaque = false
|
||||
}
|
||||
|
||||
init {
|
||||
header.add(icon, BorderLayout.WEST)
|
||||
text.next(headerText).next(descriptionText)
|
||||
header.add(text, BorderLayout.CENTER)
|
||||
syncNorth()
|
||||
@@ -153,8 +159,13 @@ class BaseQuestionView : RoundedContentPanel(
|
||||
this.icon.icon = icon
|
||||
this.icon.toolTipText = tooltip
|
||||
this.icon.isVisible = icon != null
|
||||
val attached = this.iconWrap.parent === header
|
||||
if (icon != null && !attached) header.add(iconWrap, BorderLayout.WEST)
|
||||
if (icon == null && attached) header.remove(iconWrap)
|
||||
this.icon.revalidate()
|
||||
this.icon.repaint()
|
||||
header.revalidate()
|
||||
header.repaint()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -182,7 +193,7 @@ class BaseQuestionView : RoundedContentPanel(
|
||||
fun setActions(actions: List<Action>) {
|
||||
actionButtons.clear()
|
||||
actionOrder.clear()
|
||||
this.actions.removeAll()
|
||||
mainActions.removeAll()
|
||||
for (action in actions) {
|
||||
val btn = makeButton(action.text, action.primary).apply {
|
||||
isEnabled = action.enabled
|
||||
@@ -190,7 +201,7 @@ class BaseQuestionView : RoundedContentPanel(
|
||||
}
|
||||
actionButtons[action.id] = btn
|
||||
actionOrder.add(action.id)
|
||||
this.actions.next(btn)
|
||||
mainActions.next(btn)
|
||||
}
|
||||
syncFooter()
|
||||
}
|
||||
@@ -211,9 +222,11 @@ class BaseQuestionView : RoundedContentPanel(
|
||||
@RequiresEdt
|
||||
fun setActionLeft(component: JComponent?) {
|
||||
actionLeft = component
|
||||
val old = (footer.layout as BorderLayout).getLayoutComponent(BorderLayout.WEST)
|
||||
if (old != null) footer.remove(old)
|
||||
component?.let { footer.add(it, BorderLayout.WEST) }
|
||||
sideActions.removeAll()
|
||||
component?.let {
|
||||
it.isOpaque = false
|
||||
sideActions.next(it).fill(UiStyle.Gap.pad())
|
||||
}
|
||||
syncFooter()
|
||||
}
|
||||
|
||||
@@ -226,8 +239,8 @@ class BaseQuestionView : RoundedContentPanel(
|
||||
val btn = actionButtons[id] ?: return
|
||||
if (btn.isVisible == visible) return
|
||||
btn.isVisible = visible
|
||||
actions.revalidate()
|
||||
actions.repaint()
|
||||
mainActions.revalidate()
|
||||
mainActions.repaint()
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -279,11 +292,17 @@ class BaseQuestionView : RoundedContentPanel(
|
||||
|
||||
private fun syncFooter() {
|
||||
val layout = footer.layout as BorderLayout
|
||||
val west = layout.getLayoutComponent(BorderLayout.WEST)
|
||||
val east = layout.getLayoutComponent(BorderLayout.EAST)
|
||||
if (actionLeft == null) {
|
||||
if (west != null) footer.remove(west)
|
||||
} else if (west == null) {
|
||||
footer.add(sideActions, BorderLayout.WEST)
|
||||
}
|
||||
if (actionOrder.isEmpty()) {
|
||||
if (east != null) footer.remove(east)
|
||||
} else if (east == null) {
|
||||
footer.add(actions, BorderLayout.EAST)
|
||||
footer.add(mainActions, BorderLayout.EAST)
|
||||
}
|
||||
|
||||
val root = this.layout as BorderLayout
|
||||
@@ -345,7 +364,7 @@ class BaseQuestionView : RoundedContentPanel(
|
||||
}
|
||||
|
||||
private fun applyFont(area: JBTextArea, bold: Boolean) {
|
||||
val font = if (bold) style.headerFont else style.hintFont
|
||||
val font = if (bold) UiStyle.Fonts.heading() else style.hintFont
|
||||
if (area.font != font) area.font = font
|
||||
}
|
||||
|
||||
|
||||
+75
-59
@@ -1,7 +1,9 @@
|
||||
package ai.kilocode.client.ui.layout
|
||||
|
||||
import java.awt.Component
|
||||
import java.awt.Container
|
||||
import java.awt.Dimension
|
||||
import java.awt.LayoutManager2
|
||||
import javax.swing.JPanel
|
||||
|
||||
enum class HAlign { TRACK, FIT, LEFT, CENTER, RIGHT }
|
||||
@@ -37,77 +39,91 @@ enum class VAlign { TRACK, FIT, TOP, CENTER, BOTTOM }
|
||||
*/
|
||||
class Align(
|
||||
child: Component,
|
||||
private val h: HAlign = HAlign.FIT,
|
||||
private val v: VAlign = VAlign.FIT,
|
||||
) : JPanel(null) {
|
||||
h: HAlign = HAlign.FIT,
|
||||
v: VAlign = VAlign.FIT,
|
||||
) : JPanel(Layout(h, v)) {
|
||||
|
||||
init {
|
||||
isOpaque = false
|
||||
add(child)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Layout
|
||||
// -----------------------------------------------------------------------
|
||||
private class Layout(
|
||||
private val h: HAlign,
|
||||
private val v: VAlign,
|
||||
) : LayoutManager2 {
|
||||
|
||||
override fun doLayout() {
|
||||
if (componentCount == 0) return
|
||||
val child = getComponent(0)
|
||||
val ins = insets
|
||||
val availW = maxOf(0, width - ins.left - ins.right)
|
||||
val availH = maxOf(0, height - ins.top - ins.bottom)
|
||||
override fun addLayoutComponent(comp: Component, constraints: Any?) = Unit
|
||||
override fun addLayoutComponent(name: String?, comp: Component) = Unit
|
||||
override fun removeLayoutComponent(comp: Component) = Unit
|
||||
|
||||
val min = child.minimumSize
|
||||
val max = child.maximumSize
|
||||
child.setSize(probe(h, availW, min.width, max.width), probe(v, availH, min.height, max.height))
|
||||
val pref = child.preferredSize
|
||||
override fun layoutContainer(parent: Container) {
|
||||
if (parent.componentCount == 0) return
|
||||
val child = parent.getComponent(0)
|
||||
val ins = parent.insets
|
||||
val availW = maxOf(0, parent.width - ins.left - ins.right)
|
||||
val availH = maxOf(0, parent.height - ins.top - ins.bottom)
|
||||
|
||||
val (w, cx) = placeAxis(h, availW, min.width, pref.width, max.width)
|
||||
val (ht, cy) = placeAxis(v, availH, min.height, pref.height, max.height)
|
||||
val min = child.minimumSize
|
||||
val max = child.maximumSize
|
||||
child.setSize(probe(h, availW, min.width, max.width), probe(v, availH, min.height, max.height))
|
||||
val pref = child.preferredSize
|
||||
|
||||
child.setBounds(ins.left + cx, ins.top + cy, w, ht)
|
||||
}
|
||||
val (w, cx) = place(h, availW, min.width, pref.width, max.width)
|
||||
val (ht, cy) = place(v, availH, min.height, pref.height, max.height)
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// Wrapper size negotiation
|
||||
// -----------------------------------------------------------------------
|
||||
|
||||
override fun getMinimumSize(): Dimension {
|
||||
if (componentCount == 0) return super.getMinimumSize()
|
||||
val child = getComponent(0)
|
||||
val ins = insets
|
||||
val cw = if (h == HAlign.TRACK) 0 else child.minimumSize.width
|
||||
val ch = if (v == VAlign.TRACK) 0 else child.minimumSize.height
|
||||
return Dimension(cw + ins.left + ins.right, ch + ins.top + ins.bottom)
|
||||
}
|
||||
|
||||
override fun getPreferredSize(): Dimension {
|
||||
if (componentCount == 0) return super.getPreferredSize()
|
||||
val child = getComponent(0)
|
||||
val ins = insets
|
||||
val min = child.minimumSize
|
||||
val max = child.maximumSize
|
||||
val availW = maxOf(0, width - ins.left - ins.right)
|
||||
val availH = maxOf(0, height - ins.top - ins.bottom)
|
||||
if (availW > 0 || availH > 0) {
|
||||
child.setSize(
|
||||
if (availW > 0) probe(h, availW, min.width, max.width) else child.width,
|
||||
if (availH > 0) probe(v, availH, min.height, max.height) else child.height,
|
||||
)
|
||||
child.setBounds(ins.left + cx, ins.top + cy, w, ht)
|
||||
}
|
||||
val pref = child.preferredSize
|
||||
val cw = if (h == HAlign.TRACK) 0 else bounded(pref.width, min.width, max.width)
|
||||
val ch = if (v == VAlign.TRACK) 0 else bounded(pref.height, min.height, max.height)
|
||||
return Dimension(cw + ins.left + ins.right, ch + ins.top + ins.bottom)
|
||||
}
|
||||
|
||||
override fun getMaximumSize(): Dimension {
|
||||
if (componentCount == 0) return super.getMaximumSize()
|
||||
val child = getComponent(0)
|
||||
val ins = insets
|
||||
val cw = if (h == HAlign.TRACK) super.getMaximumSize().width else maxOf(child.minimumSize.width, child.maximumSize.width) + ins.left + ins.right
|
||||
val ch = if (v == VAlign.TRACK) super.getMaximumSize().height else maxOf(child.minimumSize.height, child.maximumSize.height) + ins.top + ins.bottom
|
||||
return Dimension(cw, ch)
|
||||
override fun minimumLayoutSize(parent: Container): Dimension {
|
||||
if (parent.componentCount == 0) return Dimension(0, 0)
|
||||
val child = parent.getComponent(0)
|
||||
val ins = parent.insets
|
||||
val cw = if (h == HAlign.TRACK) 0 else child.minimumSize.width
|
||||
val ch = if (v == VAlign.TRACK) 0 else child.minimumSize.height
|
||||
return Dimension(cw + ins.left + ins.right, ch + ins.top + ins.bottom)
|
||||
}
|
||||
|
||||
override fun preferredLayoutSize(parent: Container): Dimension {
|
||||
if (parent.componentCount == 0) return Dimension(0, 0)
|
||||
val child = parent.getComponent(0)
|
||||
val ins = parent.insets
|
||||
val min = child.minimumSize
|
||||
val max = child.maximumSize
|
||||
val availW = maxOf(0, parent.width - ins.left - ins.right)
|
||||
val availH = maxOf(0, parent.height - ins.top - ins.bottom)
|
||||
if (availW > 0 || availH > 0) {
|
||||
child.setSize(
|
||||
if (availW > 0) probe(h, availW, min.width, max.width) else child.width,
|
||||
if (availH > 0) probe(v, availH, min.height, max.height) else child.height,
|
||||
)
|
||||
}
|
||||
val pref = child.preferredSize
|
||||
val cw = if (h == HAlign.TRACK) 0 else bounded(pref.width, min.width, max.width)
|
||||
val ch = if (v == VAlign.TRACK) 0 else bounded(pref.height, min.height, max.height)
|
||||
return Dimension(cw + ins.left + ins.right, ch + ins.top + ins.bottom)
|
||||
}
|
||||
|
||||
override fun maximumLayoutSize(target: Container): Dimension {
|
||||
if (target.componentCount == 0) return Dimension(Int.MAX_VALUE, Int.MAX_VALUE)
|
||||
val child = target.getComponent(0)
|
||||
val ins = target.insets
|
||||
val cw = if (h == HAlign.TRACK) {
|
||||
Int.MAX_VALUE
|
||||
} else {
|
||||
maxOf(child.minimumSize.width, child.maximumSize.width) + ins.left + ins.right
|
||||
}
|
||||
val ch = if (v == VAlign.TRACK) {
|
||||
Int.MAX_VALUE
|
||||
} else {
|
||||
maxOf(child.minimumSize.height, child.maximumSize.height) + ins.top + ins.bottom
|
||||
}
|
||||
return Dimension(cw, ch)
|
||||
}
|
||||
|
||||
override fun getLayoutAlignmentX(target: Container) = 0.5f
|
||||
override fun getLayoutAlignmentY(target: Container) = 0.5f
|
||||
override fun invalidateLayout(target: Container) = Unit
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,7 +137,7 @@ class Align(
|
||||
* - FIT: size = clamp(avail, min, max), offset = 0
|
||||
* - edge/center: size = clamp(boundedPref, 0, avail), offset positions according to alignment
|
||||
*/
|
||||
private fun placeAxis(mode: Any, avail: Int, min: Int, pref: Int, max: Int): Pair<Int, Int> {
|
||||
private fun place(mode: Any, avail: Int, min: Int, pref: Int, max: Int): Pair<Int, Int> {
|
||||
val effMax = maxOf(min, max)
|
||||
return when (mode) {
|
||||
HAlign.TRACK, VAlign.TRACK -> avail to 0
|
||||
|
||||
+30
@@ -3,8 +3,10 @@ package ai.kilocode.client.migration
|
||||
import ai.kilocode.client.session.SessionUiTestBase
|
||||
import ai.kilocode.client.session.ui.SessionRootPanel
|
||||
import ai.kilocode.client.session.ui.prompt.PromptPanel
|
||||
import ai.kilocode.client.migration.ui.MigrationItemRow
|
||||
import ai.kilocode.client.migration.ui.MigrationOverlayPanel
|
||||
import ai.kilocode.client.migration.ui.MigrationWizardPanel
|
||||
import ai.kilocode.client.ui.layout.Align
|
||||
import ai.kilocode.rpc.dto.LegacyMigrationDetectionDto
|
||||
import ai.kilocode.rpc.dto.MigrationProviderInfoDto
|
||||
import java.awt.Rectangle
|
||||
@@ -40,6 +42,16 @@ class SessionUiMigrationTest : SessionUiTestBase() {
|
||||
assertEquals(1, root.blocker.componentCount)
|
||||
}
|
||||
|
||||
fun `test visible migration state lays out content before resize`() {
|
||||
fakeMigration._state.value = MigrationUiState.Needed(detection = sampleDetection())
|
||||
settle()
|
||||
|
||||
val row = find<MigrationItemRow>(ui)
|
||||
assertTrue("migration row should be visible", row.isVisible)
|
||||
assertTrue("migration row width should be laid out before resize: ${row.bounds}", row.width > 0)
|
||||
assertTrue("migration row height should be laid out before resize: ${row.bounds}", row.height > 0)
|
||||
}
|
||||
|
||||
fun `test migration opens on selection screen with keep file checked`() {
|
||||
fakeMigration._state.value = MigrationUiState.Needed(detection = sampleDetection())
|
||||
settle()
|
||||
@@ -48,6 +60,23 @@ class SessionUiMigrationTest : SessionUiTestBase() {
|
||||
assertTrue(wizard.keepLegacySettingsFileSelectedForTest())
|
||||
}
|
||||
|
||||
fun `test migration wizard is centered in overlay`() {
|
||||
fakeMigration._state.value = MigrationUiState.Needed(detection = sampleDetection())
|
||||
settle()
|
||||
layout()
|
||||
|
||||
val overlay = find<MigrationOverlayPanel>(ui)
|
||||
overlay.doLayout()
|
||||
val align = find<Align>(overlay)
|
||||
align.doLayout()
|
||||
val wizard = find<MigrationWizardPanel>(overlay)
|
||||
|
||||
assertTrue("align wrapper should fill most overlay width", align.width > overlay.width / 2)
|
||||
assertTrue("align wrapper should fill most overlay height", align.height > overlay.height / 2)
|
||||
assertTrue("wizard should be horizontally centered: ${wizard.bounds} in ${align.bounds}", kotlin.math.abs(wizard.x - (align.width - wizard.width) / 2) <= 1)
|
||||
assertTrue("wizard should be vertically centered: ${wizard.bounds} in ${align.bounds}", kotlin.math.abs(wizard.y - (align.height - wizard.height) / 2) <= 1)
|
||||
}
|
||||
|
||||
fun `test hidden state after visible hides blocker`() {
|
||||
val root = find<SessionRootPanel>(ui)
|
||||
fakeMigration._state.value = MigrationUiState.Needed(detection = sampleDetection())
|
||||
@@ -108,4 +137,5 @@ class SessionUiMigrationTest : SessionUiTestBase() {
|
||||
settings = null,
|
||||
hasData = true,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
+40
-5
@@ -3,6 +3,7 @@ package ai.kilocode.client.session.views.base
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.session.ui.style.SessionUiStyle
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.layout.Align
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ide.ui.laf.darcula.ui.DarculaButtonUI
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
@@ -164,7 +165,7 @@ class BaseQuestionViewTest : BasePlatformTestCase() {
|
||||
|
||||
val north = region(panel, BorderLayout.NORTH) as Container
|
||||
val filler = north.components.last()
|
||||
assertEquals(UiStyle.Gap.pad(), filler.preferredSize.height)
|
||||
assertEquals(UiStyle.Gap.md(), filler.preferredSize.height)
|
||||
assertEquals(0, filler.preferredSize.width)
|
||||
}
|
||||
}
|
||||
@@ -255,15 +256,26 @@ class BaseQuestionViewTest : BasePlatformTestCase() {
|
||||
fun `test header row uses icon west and text stack center`() {
|
||||
edt {
|
||||
val panel = BaseQuestionView()
|
||||
panel.setHeaderIcon(AllIcons.General.Warning)
|
||||
val header = headerRow(panel)!!
|
||||
val layout = header.layout as BorderLayout
|
||||
val west = layout.getLayoutComponent(BorderLayout.WEST)
|
||||
val center = layout.getLayoutComponent(BorderLayout.CENTER) as Container
|
||||
assertTrue("icon should be a JBLabel", west is JBLabel)
|
||||
assertTrue("icon should be top-aligned", west is Align)
|
||||
assertTrue("icon wrapper should contain a JBLabel", findAll<JBLabel>(west as Container).isNotEmpty())
|
||||
assertTrue("center should contain header and description text", findAll<JBTextArea>(center).size >= 2)
|
||||
}
|
||||
}
|
||||
|
||||
fun `test header row has no west icon gap by default`() {
|
||||
edt {
|
||||
val panel = BaseQuestionView()
|
||||
val header = headerRow(panel)!!
|
||||
val west = (header.layout as BorderLayout).getLayoutComponent(BorderLayout.WEST)
|
||||
assertNull("header should not reserve icon space when icon is absent", west)
|
||||
}
|
||||
}
|
||||
|
||||
fun `test action footer is in south with buttons east`() {
|
||||
edt {
|
||||
val panel = BaseQuestionView()
|
||||
@@ -281,8 +293,31 @@ class BaseQuestionViewTest : BasePlatformTestCase() {
|
||||
val left = JLabel("left")
|
||||
panel.setActionLeft(left)
|
||||
val footer = region(panel, BorderLayout.SOUTH) as JPanel
|
||||
val west = (footer.layout as BorderLayout).getLayoutComponent(BorderLayout.WEST)
|
||||
assertSame("action left should be in footer west", left, west)
|
||||
val west = (footer.layout as BorderLayout).getLayoutComponent(BorderLayout.WEST) as Container
|
||||
assertNotNull("action left should be in footer west", find(west, left))
|
||||
}
|
||||
}
|
||||
|
||||
fun `test action left component is transparent`() {
|
||||
edt {
|
||||
val panel = BaseQuestionView()
|
||||
val left = JPanel()
|
||||
panel.setActionLeft(left)
|
||||
assertFalse("action left should be transparent", left.isOpaque)
|
||||
}
|
||||
}
|
||||
|
||||
fun `test footer adds bottom padding gap after side actions`() {
|
||||
edt {
|
||||
val panel = BaseQuestionView()
|
||||
panel.setActionLeft(JLabel("left"))
|
||||
panel.setActions(listOf(BaseQuestionView.Action("ok", "OK", primary = true) {}))
|
||||
|
||||
val footer = region(panel, BorderLayout.SOUTH) as JPanel
|
||||
val west = (footer.layout as BorderLayout).getLayoutComponent(BorderLayout.WEST) as Container
|
||||
val filler = west.components.toList().firstOrNull { it.preferredSize.width == UiStyle.Gap.pad() }
|
||||
assertNotNull("side actions should include trailing gap", filler)
|
||||
assertEquals(0, filler!!.preferredSize.height)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -329,7 +364,7 @@ class BaseQuestionViewTest : BasePlatformTestCase() {
|
||||
val style = SessionEditorStyle.current()
|
||||
panel.applyStyle(style)
|
||||
|
||||
assertEquals("headerText should use headerFont", style.headerFont, panel.headerFont())
|
||||
assertEquals("headerText should use heading font", UiStyle.Fonts.heading(), panel.headerFont())
|
||||
assertEquals("descriptionText should use hintFont", style.hintFont, panel.descriptionFont())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user