mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-09-24 16:02:55 +08:00
feat(jetbrains): use active list for history rows
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@kilocode/kilo-jetbrains": patch
|
||||
---
|
||||
|
||||
Use the shared active list for session history rows with hover-revealed local delete actions.
|
||||
-165
@@ -1,165 +0,0 @@
|
||||
package ai.kilocode.client.session.history
|
||||
|
||||
import ai.kilocode.client.session.ui.PickerRow
|
||||
import ai.kilocode.client.session.SessionActivityKind
|
||||
import ai.kilocode.client.ui.FilledBadgeIcon
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ui.GroupHeaderSeparator
|
||||
import com.intellij.ui.SimpleColoredComponent
|
||||
import com.intellij.ui.SimpleTextAttributes
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.util.ui.EmptyIcon
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Point
|
||||
import java.awt.Rectangle
|
||||
import java.awt.Component
|
||||
import javax.swing.Icon
|
||||
import javax.swing.JList
|
||||
import javax.swing.JPanel
|
||||
import javax.swing.ListCellRenderer
|
||||
import javax.swing.SwingConstants
|
||||
|
||||
private const val DELETE_AREA_WIDTH = 32
|
||||
|
||||
internal open class HistoryRenderer<T : HistoryItem>(
|
||||
private val model: HistoryModel<T>,
|
||||
private val deletable: Boolean,
|
||||
private val activity: () -> Map<String, SessionActivityKind>,
|
||||
private val titles: () -> Map<String, String> = { emptyMap() },
|
||||
) : JPanel(BorderLayout()), ListCellRenderer<T> {
|
||||
companion object {
|
||||
private val icon: Icon = AllIcons.Actions.GC
|
||||
private val empty: Icon = EmptyIcon.create(icon)
|
||||
|
||||
fun isDeleteClick(list: JList<*>, bounds: Rectangle, point: Point): Boolean {
|
||||
val width = JBUI.scale(DELETE_AREA_WIDTH)
|
||||
return if (list.componentOrientation.isLeftToRight) {
|
||||
val right = bounds.x + bounds.width
|
||||
point.x in (right - width)..right
|
||||
} else {
|
||||
val left = bounds.x
|
||||
point.x in left..(left + width)
|
||||
}
|
||||
}
|
||||
|
||||
fun section(items: List<HistoryItem>, index: Int): String? {
|
||||
val item = items.getOrNull(index) ?: return null
|
||||
val current = HistoryTime.section(item)
|
||||
val previous = items.getOrNull(index - 1)?.let(HistoryTime::section)
|
||||
if (current == previous) return null
|
||||
return HistoryTime.title(current)
|
||||
}
|
||||
}
|
||||
|
||||
private val sep = GroupHeaderSeparator(JBUI.CurrentTheme.Popup.separatorLabelInsets())
|
||||
private val top = JPanel(BorderLayout()).apply {
|
||||
border = JBUI.Borders.empty()
|
||||
add(sep, BorderLayout.NORTH)
|
||||
}
|
||||
private val title = SimpleColoredComponent()
|
||||
private val badge = BadgeLabel()
|
||||
private val time = JBLabel()
|
||||
private val del = JBLabel().apply {
|
||||
horizontalAlignment = SwingConstants.CENTER
|
||||
verticalAlignment = SwingConstants.CENTER
|
||||
border = JBUI.Borders.emptyLeft(JBUI.CurrentTheme.ActionsList.elementIconGap())
|
||||
}
|
||||
private val head = JPanel(BorderLayout(UiStyle.Gap.xs(), 0)).apply {
|
||||
add(title, BorderLayout.CENTER)
|
||||
add(badge, BorderLayout.EAST)
|
||||
}
|
||||
private val main = JPanel(BorderLayout(UiStyle.Gap.md(), 0)).apply {
|
||||
add(head, BorderLayout.CENTER)
|
||||
add(time, BorderLayout.EAST)
|
||||
}
|
||||
private val row = JPanel(BorderLayout()).apply {
|
||||
add(main, BorderLayout.CENTER)
|
||||
if (deletable) add(del, BorderLayout.EAST)
|
||||
}
|
||||
private val wrap = PickerRow()
|
||||
private var text = ""
|
||||
|
||||
init {
|
||||
isOpaque = true
|
||||
top.isOpaque = true
|
||||
row.border = JBUI.Borders.empty(
|
||||
UiStyle.Gap.lg(),
|
||||
UiStyle.Gap.lg(),
|
||||
UiStyle.Gap.lg(),
|
||||
UiStyle.Gap.lg(),
|
||||
)
|
||||
UiStyle.Components.transparent(row, main, head, title, badge, time, del)
|
||||
wrap.setContent(row)
|
||||
add(top, BorderLayout.NORTH)
|
||||
add(wrap, BorderLayout.CENTER)
|
||||
}
|
||||
|
||||
override fun getListCellRendererComponent(
|
||||
list: JList<out T>,
|
||||
value: T?,
|
||||
index: Int,
|
||||
selected: Boolean,
|
||||
focus: Boolean,
|
||||
): JPanel {
|
||||
val focused = selected || list.hasFocus() || focus
|
||||
val fg = UIUtil.getListForeground(selected, focused)
|
||||
val weak = if (selected) fg else UIUtil.getContextHelpForeground()
|
||||
|
||||
background = list.background
|
||||
top.background = list.background
|
||||
wrap.update(list, selected, focused)
|
||||
sep.caption = section(model.visibleItems, index)
|
||||
sep.setHideLine(index == 0)
|
||||
top.isVisible = sep.caption != null
|
||||
|
||||
title.clear()
|
||||
text = value?.let { titles()[it.id] ?: title(it) }.orEmpty()
|
||||
title.append(
|
||||
text,
|
||||
SimpleTextAttributes(SimpleTextAttributes.STYLE_BOLD, fg),
|
||||
)
|
||||
time.text = value?.let(HistoryTime::relative).orEmpty()
|
||||
time.foreground = weak
|
||||
badge.setKind(value?.id?.let(activity()::get))
|
||||
if (deletable) del.icon = if (selected) icon else empty
|
||||
|
||||
top.invalidate()
|
||||
return this
|
||||
}
|
||||
|
||||
internal fun runningVisible() = badge.isVisible
|
||||
|
||||
internal fun badgeText() = badge.kind?.label()
|
||||
|
||||
internal fun titleText() = text
|
||||
|
||||
private class BadgeLabel : JBLabel() {
|
||||
var kind: SessionActivityKind? = null
|
||||
private set
|
||||
|
||||
init {
|
||||
border = JBUI.Borders.emptyLeft(JBUI.CurrentTheme.ActionsList.elementIconGap())
|
||||
alignmentY = Component.CENTER_ALIGNMENT
|
||||
}
|
||||
|
||||
fun setKind(value: SessionActivityKind?) {
|
||||
kind = value
|
||||
isVisible = value != null
|
||||
icon = value?.let { FilledBadgeIcon(it.label(), it.style()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal class LocalHistoryRenderer(
|
||||
model: HistoryModel<LocalHistoryItem>,
|
||||
activity: () -> Map<String, SessionActivityKind> = { emptyMap() },
|
||||
titles: () -> Map<String, String> = { emptyMap() },
|
||||
) : HistoryRenderer<LocalHistoryItem>(model, deletable = true, activity, titles)
|
||||
|
||||
internal class CloudHistoryRenderer(
|
||||
model: HistoryModel<CloudHistoryItem>,
|
||||
activity: () -> Map<String, SessionActivityKind> = { emptyMap() },
|
||||
) : HistoryRenderer<CloudHistoryItem>(model, deletable = false, activity)
|
||||
+114
-170
@@ -4,35 +4,35 @@ import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.SessionManager
|
||||
import ai.kilocode.client.session.ui.LoadingPanel
|
||||
import ai.kilocode.client.session.ui.style.SessionEditorStyle
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import ai.kilocode.client.ui.HoverIcon
|
||||
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 ai.kilocode.client.ui.list.ActiveList
|
||||
import ai.kilocode.client.ui.list.ActiveListConfig
|
||||
import ai.kilocode.client.ui.list.ActiveListDeleteOptions
|
||||
import ai.kilocode.client.ui.list.ActiveListItem
|
||||
import ai.kilocode.client.ui.list.ActiveListSelection
|
||||
import ai.kilocode.client.util.UiTimerSource
|
||||
import ai.kilocode.client.util.UiTimers
|
||||
import com.intellij.icons.AllIcons
|
||||
import com.intellij.ide.ui.LafManagerListener
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.Disposable
|
||||
import com.intellij.openapi.actionSystem.ActionGroup
|
||||
import com.intellij.openapi.actionSystem.ActionManager
|
||||
import com.intellij.openapi.actionSystem.ActionPlaces
|
||||
import com.intellij.openapi.actionSystem.DataProvider
|
||||
import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.ui.DocumentAdapter
|
||||
import com.intellij.ui.PopupHandler
|
||||
import com.intellij.ui.SearchTextField
|
||||
import com.intellij.ui.ScrollingUtil
|
||||
import com.intellij.ui.components.JBCheckBox
|
||||
import com.intellij.ui.components.JBList
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import com.intellij.ui.tabs.JBTabs
|
||||
import com.intellij.ui.tabs.JBTabsFactory
|
||||
import com.intellij.ui.tabs.JBTabsPosition
|
||||
import com.intellij.ui.tabs.TabInfo
|
||||
import com.intellij.ui.tabs.TabsListener
|
||||
import com.intellij.util.concurrency.annotations.RequiresEdt
|
||||
import com.intellij.util.ui.Centerizer
|
||||
import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
@@ -49,10 +49,8 @@ import java.awt.event.MouseAdapter
|
||||
import java.awt.event.MouseEvent
|
||||
import javax.swing.JButton
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.JList
|
||||
import javax.swing.KeyStroke
|
||||
import javax.swing.ListSelectionModel
|
||||
import javax.swing.ScrollPaneConstants
|
||||
import javax.swing.SwingUtilities
|
||||
import javax.swing.event.DocumentEvent
|
||||
import javax.swing.event.ListDataEvent
|
||||
@@ -68,6 +66,8 @@ class HistoryPanel(
|
||||
private val localSearch = search(controller.local)
|
||||
private val cloudSearch = search(controller.cloud)
|
||||
private var snapshot = HistoryActivitySnapshot()
|
||||
private var localRows = emptyList<LocalHistoryRow>()
|
||||
private var cloudRows = emptyList<CloudHistoryRow>()
|
||||
private val localList = localList()
|
||||
private val cloudList = cloudList()
|
||||
private val more = LoadMoreButton()
|
||||
@@ -106,8 +106,8 @@ class HistoryPanel(
|
||||
Disposer.register(parent, this)
|
||||
border = JBUI.Borders.empty(UiStyle.Gap.lg(), UiStyle.Gap.lg(), UiStyle.Gap.lg(), 0)
|
||||
more.addActionListener { controller.loadMoreCloud() }
|
||||
bind(localList, controller.local)
|
||||
bind(cloudList, controller.cloud)
|
||||
bind(controller.local)
|
||||
bind(controller.cloud)
|
||||
bindTheme()
|
||||
controller.onRepoOnlyChanged = { value ->
|
||||
repoOnly.isSelected = value
|
||||
@@ -154,16 +154,9 @@ class HistoryPanel(
|
||||
SwingUtilities.updateComponentTreeUI(localPanel)
|
||||
SwingUtilities.updateComponentTreeUI(cloudPanel)
|
||||
load.applyStyle(SessionEditorStyle.current())
|
||||
updateRenderer(localList)
|
||||
updateRenderer(cloudList)
|
||||
sync()
|
||||
}
|
||||
|
||||
private fun updateRenderer(list: JBList<out HistoryItem>) {
|
||||
val view = list.cellRenderer
|
||||
if (view is JComponent) SwingUtilities.updateComponentTreeUI(view)
|
||||
}
|
||||
|
||||
private fun search(model: HistoryModel<out HistoryItem>) = SearchTextField(false).apply {
|
||||
textEditor.emptyText.text = KiloBundle.message("history.search.placeholder")
|
||||
textEditor.document.addDocumentListener(object : DocumentAdapter() {
|
||||
@@ -182,7 +175,7 @@ class HistoryPanel(
|
||||
JComponent.WHEN_FOCUSED,
|
||||
)
|
||||
textEditor.registerKeyboardAction(
|
||||
{ activeList().selectedValue?.let(::activate) },
|
||||
{ activeList().selected()?.let(::activate) },
|
||||
KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0),
|
||||
JComponent.WHEN_FOCUSED,
|
||||
)
|
||||
@@ -202,20 +195,17 @@ class HistoryPanel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun panel(search: SearchTextField, list: JList<out HistoryItem>, footer: JComponent? = null): JComponent {
|
||||
private fun panel(search: SearchTextField, list: ActiveList, footer: JComponent? = null): JComponent {
|
||||
return BorderLayoutPanel().apply {
|
||||
val north = BorderLayoutPanel().apply {
|
||||
add(search, BorderLayout.CENTER)
|
||||
if (list === cloudList) {
|
||||
add(repoOnly, BorderLayout.SOUTH)
|
||||
}
|
||||
if (list === cloudList) add(repoOnly, BorderLayout.SOUTH)
|
||||
border = JBUI.Borders.emptyRight(UiStyle.Gap.lg())
|
||||
}
|
||||
add(north, BorderLayout.NORTH)
|
||||
add(JBScrollPane(list).apply {
|
||||
border = JBUI.Borders.empty()
|
||||
viewportBorder = JBUI.Borders.emptyRight(UiStyle.Gap.lg())
|
||||
horizontalScrollBarPolicy = ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER
|
||||
add(BorderLayoutPanel().apply {
|
||||
border = JBUI.Borders.emptyRight(UiStyle.Gap.lg())
|
||||
addToCenter(list)
|
||||
}, BorderLayout.CENTER)
|
||||
footer?.let {
|
||||
add(Centerizer(it, Centerizer.TYPE.HORIZONTAL).apply {
|
||||
@@ -225,55 +215,35 @@ class HistoryPanel(
|
||||
}
|
||||
}
|
||||
|
||||
private fun localList() = HistoryList(controller.local).apply {
|
||||
selectionMode = ListSelectionModel.MULTIPLE_INTERVAL_SELECTION
|
||||
isFocusable = true
|
||||
cellRenderer = LocalHistoryRenderer(controller.local, { snapshot.activity }, { snapshot.titles })
|
||||
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||
emptyText.text = KiloBundle.message("history.empty")
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
val row = locationToIndex(e.point)
|
||||
val box = row.takeIf { it >= 0 }?.let { getCellBounds(it, it) } ?: return
|
||||
if (!box.contains(e.point)) return
|
||||
if (e.clickCount == 1 && HistoryRenderer.isDeleteClick(this@apply, box, e.point)) {
|
||||
val item = model.getElementAt(row)
|
||||
confirm(item)
|
||||
} else if (e.clickCount == 2) {
|
||||
selectedValue?.let(::activate)
|
||||
}
|
||||
}
|
||||
})
|
||||
registerKeyboardAction(
|
||||
{ selectedValue?.let(::activate) },
|
||||
KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0),
|
||||
JComponent.WHEN_FOCUSED,
|
||||
)
|
||||
private fun localList() = ActiveList(
|
||||
KiloBundle.message("history.empty"),
|
||||
cfg = ActiveListConfig(selection = ListSelectionModel.MULTIPLE_INTERVAL_SELECTION, hoverActions = true),
|
||||
showSearch = false,
|
||||
openOnClick = false,
|
||||
onCell = { key, id ->
|
||||
if (id != HISTORY_DELETE_CELL) return@ActiveList
|
||||
val item = localRows.firstOrNull { it.key == key }?.item ?: return@ActiveList
|
||||
showDeletePopup(listOf(item), HISTORY_DELETE_CELL)
|
||||
},
|
||||
onOpen = { row, _ -> activate(row) },
|
||||
).apply {
|
||||
setListCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR))
|
||||
installContextMenu(this)
|
||||
ScrollingUtil.installActions(this)
|
||||
}
|
||||
|
||||
private fun cloudList() = HistoryList(controller.cloud).apply {
|
||||
selectionMode = ListSelectionModel.SINGLE_SELECTION
|
||||
isFocusable = true
|
||||
cellRenderer = CloudHistoryRenderer(controller.cloud) { snapshot.activity }
|
||||
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
|
||||
emptyText.text = KiloBundle.message("history.empty")
|
||||
addMouseListener(object : MouseAdapter() {
|
||||
override fun mouseClicked(e: MouseEvent) {
|
||||
if (e.clickCount == 2) selectedValue?.let(::activate)
|
||||
}
|
||||
})
|
||||
registerKeyboardAction(
|
||||
{ selectedValue?.let(::activate) },
|
||||
KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0),
|
||||
JComponent.WHEN_FOCUSED,
|
||||
)
|
||||
private fun cloudList() = ActiveList(
|
||||
KiloBundle.message("history.empty"),
|
||||
cfg = ActiveListConfig(selection = ListSelectionModel.SINGLE_SELECTION),
|
||||
showSearch = false,
|
||||
openOnClick = false,
|
||||
onCell = { _, _ -> },
|
||||
onOpen = { row, _ -> activate(row) },
|
||||
).apply {
|
||||
setListCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR))
|
||||
installContextMenu(this)
|
||||
ScrollingUtil.installActions(this)
|
||||
}
|
||||
|
||||
private fun <T : HistoryItem> bind(list: JBList<T>, model: HistoryModel<T>) {
|
||||
private fun bind(model: HistoryModel<out HistoryItem>) {
|
||||
val listener = object : ListDataListener {
|
||||
override fun intervalAdded(e: ListDataEvent) = sync()
|
||||
|
||||
@@ -283,10 +253,10 @@ class HistoryPanel(
|
||||
}
|
||||
model.addListDataListener(listener)
|
||||
Disposer.register(this) { model.removeListDataListener(listener) }
|
||||
list.setPaintBusy(model.loading)
|
||||
}
|
||||
|
||||
private fun sync() {
|
||||
syncRows()
|
||||
syncList(localList, controller.local)
|
||||
syncList(cloudList, controller.cloud)
|
||||
more.isEnabled = controller.cloud.cursor != null && !controller.cloud.loading
|
||||
@@ -297,24 +267,33 @@ class HistoryPanel(
|
||||
repaint()
|
||||
}
|
||||
|
||||
private fun syncRows() {
|
||||
localRows = localHistoryRows(controller.local.visibleItems, snapshot, controller::deleting)
|
||||
cloudRows = cloudHistoryRows(controller.cloud.visibleItems, snapshot)
|
||||
restore(localList, localRows)
|
||||
restore(cloudList, cloudRows)
|
||||
}
|
||||
|
||||
private fun restore(list: ActiveList, rows: List<ActiveListItem>) {
|
||||
val keys = list.selectedKeys()
|
||||
list.update(rows, ActiveListSelection.PreserveNoScroll)
|
||||
val indices = keys.mapNotNull { key -> rows.indexOfFirst { it.key == key }.takeIf { it >= 0 } }.toIntArray()
|
||||
if (indices.isEmpty()) {
|
||||
list.clearSelection()
|
||||
return
|
||||
}
|
||||
list.setSelectionIndices(indices)
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
internal fun syncActivity() {
|
||||
val next = HistoryActivitySnapshot(
|
||||
activity = manager?.activity() ?: controller.activity(),
|
||||
titles = manager?.titles().orEmpty(),
|
||||
)
|
||||
val changed = snapshot.changed(next)
|
||||
if (snapshot.changed(next).isEmpty()) return
|
||||
snapshot = next
|
||||
repaintRows(localList, controller.local, changed)
|
||||
repaintRows(cloudList, controller.cloud, changed)
|
||||
}
|
||||
|
||||
private fun <T : HistoryItem> repaintRows(list: JBList<T>, model: HistoryModel<T>, ids: Set<String>) {
|
||||
if (ids.isEmpty()) return
|
||||
model.visibleItems.forEachIndexed { index, item ->
|
||||
if (item.id !in ids) return@forEachIndexed
|
||||
list.getCellBounds(index, index)?.let(list::repaint)
|
||||
}
|
||||
syncRows()
|
||||
}
|
||||
|
||||
private fun loading(): Boolean {
|
||||
@@ -322,19 +301,19 @@ class HistoryPanel(
|
||||
return controller.local.loading || controller.cloud.loading
|
||||
}
|
||||
|
||||
private fun <T : HistoryItem> syncList(list: JBList<T>, model: HistoryModel<T>) {
|
||||
list.setPaintBusy(model.loading)
|
||||
list.emptyText.text = when {
|
||||
private fun syncList(list: ActiveList, model: HistoryModel<out HistoryItem>) {
|
||||
list.setBusy(model.loading)
|
||||
list.setEmptyText(when {
|
||||
model.loading -> KiloBundle.message("history.loading")
|
||||
model.error != null -> model.error.orEmpty()
|
||||
else -> KiloBundle.message("history.empty")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun activate(item: HistoryItem) {
|
||||
when (item) {
|
||||
is LocalHistoryItem -> controller.open(item)
|
||||
is CloudHistoryItem -> controller.open(item)
|
||||
private fun activate(row: ActiveListItem) {
|
||||
when (row) {
|
||||
is LocalHistoryRow -> controller.open(row.item)
|
||||
is CloudHistoryRow -> controller.open(row.item)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -343,70 +322,66 @@ class HistoryPanel(
|
||||
if (HistoryDataKeys.CONTROLLER.`is`(dataId)) return controller
|
||||
if (HistoryDataKeys.SELECTION.`is`(dataId)) {
|
||||
val source = selectedSource()
|
||||
val local = if (source == HistorySource.LOCAL) localList.selectedValuesList.filterIsInstance<LocalHistoryItem>() else emptyList()
|
||||
val cloud = if (source == HistorySource.CLOUD) cloudList.selectedValuesList.filterIsInstance<CloudHistoryItem>() else emptyList()
|
||||
val local = if (source == HistorySource.LOCAL) {
|
||||
localList.selectedItems().filterIsInstance<LocalHistoryRow>().map { it.item }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
val cloud = if (source == HistorySource.CLOUD) {
|
||||
cloudList.selectedItems().filterIsInstance<CloudHistoryRow>().map { it.item }
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
return HistorySelection(source, local, cloud)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun installContextMenu(list: JBList<out HistoryItem>) {
|
||||
private fun installContextMenu(list: ActiveList) {
|
||||
val group = ActionManager.getInstance().getAction("Kilo.History.ContextMenu")
|
||||
if (group is ActionGroup) {
|
||||
PopupHandler.installPopupMenu(list, group, ActionPlaces.POPUP)
|
||||
}
|
||||
if (group is ActionGroup) list.installPopup(group)
|
||||
}
|
||||
|
||||
private fun confirm(item: LocalHistoryItem) {
|
||||
if (controller.deleting(item)) return
|
||||
val result = com.intellij.openapi.ui.Messages.showYesNoDialog(
|
||||
this,
|
||||
KiloBundle.message("history.delete.confirm.message", title(item)),
|
||||
KiloBundle.message("history.delete.confirm.title"),
|
||||
com.intellij.openapi.ui.Messages.getWarningIcon(),
|
||||
)
|
||||
if (result != com.intellij.openapi.ui.Messages.YES) return
|
||||
controller.delete(item)
|
||||
private fun showDeletePopup(items: List<LocalHistoryItem>, cell: String? = null) {
|
||||
val active = items.filter { !controller.deleting(it) }
|
||||
if (active.isEmpty()) return
|
||||
val msg = if (active.size == 1) {
|
||||
KiloBundle.message("history.delete.confirm.message", title(active[0]))
|
||||
} else {
|
||||
KiloBundle.message("history.delete.confirm.message.multiple", active.size)
|
||||
}
|
||||
controller.requestDelete(active.size)
|
||||
localList.confirmDelete(
|
||||
localList.point(active[0].id, cell),
|
||||
ActiveListDeleteOptions(message = msg),
|
||||
) { active.forEach(controller::delete) }
|
||||
}
|
||||
|
||||
internal fun confirmDelete(items: List<LocalHistoryItem>) {
|
||||
val active = items.filter { !controller.deleting(it) }
|
||||
if (active.isEmpty()) return
|
||||
val msg = if (active.size == 1)
|
||||
KiloBundle.message("history.delete.confirm.message", title(active[0]))
|
||||
else
|
||||
KiloBundle.message("history.delete.confirm.message.multiple", active.size)
|
||||
val result = com.intellij.openapi.ui.Messages.showYesNoDialog(
|
||||
this,
|
||||
msg,
|
||||
KiloBundle.message("history.delete.confirm.title"),
|
||||
com.intellij.openapi.ui.Messages.getWarningIcon(),
|
||||
)
|
||||
if (result != com.intellij.openapi.ui.Messages.YES) return
|
||||
active.forEach { controller.delete(it) }
|
||||
showDeletePopup(items)
|
||||
}
|
||||
|
||||
internal fun itemCount() = activeModel().size
|
||||
internal fun itemCount() = activeRows().size
|
||||
|
||||
internal fun selectedSource() = if (tabs.selectedInfo === cloudInfo) HistorySource.CLOUD else HistorySource.LOCAL
|
||||
|
||||
internal fun select(index: Int) {
|
||||
activeList().selectedIndex = index
|
||||
activeList().selectIndex(index)
|
||||
}
|
||||
|
||||
internal fun selectIndices(vararg indices: Int) {
|
||||
activeList().selectedIndices = indices
|
||||
activeList().setSelectionIndices(indices)
|
||||
}
|
||||
|
||||
internal fun selectedIndex() = activeList().selectedIndex
|
||||
internal fun selectedIndex() = activeList().selectedIndex()
|
||||
|
||||
internal fun listFocusable() = activeList().isFocusable
|
||||
internal fun listFocusable() = activeList().preferredFocus().isFocusable
|
||||
|
||||
internal fun listSelectionMode() = activeList().selectionMode
|
||||
internal fun listSelectionMode() = (activeList().preferredFocus() as javax.swing.JList<*>).selectionMode
|
||||
|
||||
internal fun loadMoreFocusable() = more.isFocusable
|
||||
|
||||
internal fun listCursor() = activeList().cursor.type
|
||||
internal fun listCursor() = activeList().preferredFocus().cursor.type
|
||||
|
||||
internal fun backText(): String? {
|
||||
val view = activeInfo().foreSideComponent ?: return null
|
||||
@@ -424,8 +399,8 @@ class HistoryPanel(
|
||||
}
|
||||
|
||||
internal fun clickDelete() {
|
||||
val items = localList.selectedValuesList.filterIsInstance<LocalHistoryItem>()
|
||||
items.forEach { controller.delete(it) }
|
||||
val items = localList.selectedItems().filterIsInstance<LocalHistoryRow>().map { it.item }
|
||||
showDeletePopup(items)
|
||||
}
|
||||
|
||||
internal fun clickCloud() {
|
||||
@@ -446,38 +421,13 @@ class HistoryPanel(
|
||||
if (tabs.selectedInfo === cloudInfo) cloudSearch.text = value else localSearch.text = value
|
||||
}
|
||||
|
||||
internal fun groupTitles(): List<String> {
|
||||
val items = activeModel().visibleItems
|
||||
return items.indices.mapNotNull { HistoryRenderer.section(items, it) }
|
||||
}
|
||||
internal fun groupTitles(): List<String> = activeRows().mapNotNull { it.section }
|
||||
|
||||
internal fun runningBadgeVisible(index: Int): Boolean {
|
||||
return badgeText(index) != null
|
||||
}
|
||||
internal fun runningBadgeVisible(index: Int): Boolean = activeRows().getOrNull(index)?.badges?.isNotEmpty() == true
|
||||
|
||||
internal fun badgeText(index: Int): String? {
|
||||
val list = activeList()
|
||||
val item = list.model.getElementAt(index) ?: return null
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val renderer = list.cellRenderer as javax.swing.ListCellRenderer<HistoryItem>
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val typed = list as JList<HistoryItem>
|
||||
val view = renderer.getListCellRendererComponent(typed, item, index, false, false)
|
||||
if (view !is HistoryRenderer<*>) return null
|
||||
return view.badgeText()
|
||||
}
|
||||
internal fun badgeText(index: Int): String? = activeRows().getOrNull(index)?.badges?.firstOrNull()?.text
|
||||
|
||||
internal fun titleText(index: Int): String? {
|
||||
val list = activeList()
|
||||
val item = list.model.getElementAt(index) ?: return null
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val renderer = list.cellRenderer as javax.swing.ListCellRenderer<HistoryItem>
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val typed = list as JList<HistoryItem>
|
||||
val view = renderer.getListCellRendererComponent(typed, item, index, false, false)
|
||||
if (view !is HistoryRenderer<*>) return null
|
||||
return view.titleText()
|
||||
}
|
||||
internal fun titleText(index: Int): String? = activeRows().getOrNull(index)?.title
|
||||
|
||||
internal fun repoOnlyVisible() = repoOnly.isVisible
|
||||
|
||||
@@ -487,22 +437,20 @@ class HistoryPanel(
|
||||
repoOnly.doClick()
|
||||
}
|
||||
|
||||
private fun activeList(): JBList<out HistoryItem> = if (tabs.selectedInfo === cloudInfo) cloudList else localList
|
||||
private fun activeList(): ActiveList = if (tabs.selectedInfo === cloudInfo) cloudList else localList
|
||||
|
||||
private fun activeModel(): HistoryModel<out HistoryItem> = if (tabs.selectedInfo === cloudInfo) controller.cloud else controller.local
|
||||
private fun activeRows(): List<ActiveListItem> = if (tabs.selectedInfo === cloudInfo) cloudRows else localRows
|
||||
|
||||
private fun activeSearch(): SearchTextField = if (tabs.selectedInfo === cloudInfo) cloudSearch else localSearch
|
||||
|
||||
private fun activeInfo(): TabInfo = if (tabs.selectedInfo === cloudInfo) cloudInfo else localInfo
|
||||
|
||||
private fun move(step: Int) {
|
||||
val list = activeList()
|
||||
val size = list.model.size
|
||||
val size = itemCount()
|
||||
if (size <= 0) return
|
||||
val cur = list.selectedIndex.takeIf { it >= 0 } ?: if (step > 0) -1 else size
|
||||
val cur = selectedIndex().takeIf { it >= 0 } ?: if (step > 0) -1 else size
|
||||
val idx = (cur + step).coerceIn(0, size - 1)
|
||||
list.selectedIndex = idx
|
||||
ScrollingUtil.ensureIndexIsVisible(list, idx, 0)
|
||||
activeList().selectIndex(idx)
|
||||
}
|
||||
|
||||
override fun dispose() {
|
||||
@@ -552,10 +500,6 @@ class HistoryPanel(
|
||||
}
|
||||
}
|
||||
|
||||
private class HistoryList<T : HistoryItem>(model: HistoryModel<T>) : JBList<T>(model) {
|
||||
override fun getScrollableTracksViewportWidth() = true
|
||||
}
|
||||
|
||||
internal fun showingLoading() = !controller.local.loaded && !controller.cloud.loaded && (controller.local.loading || controller.cloud.loading)
|
||||
|
||||
private companion object {
|
||||
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
package ai.kilocode.client.session.history
|
||||
|
||||
import ai.kilocode.client.plugin.KiloBundle
|
||||
import ai.kilocode.client.session.SessionActivityKind
|
||||
import ai.kilocode.client.ui.list.ActiveListBadge
|
||||
import ai.kilocode.client.ui.list.ActiveListCell
|
||||
import ai.kilocode.client.ui.list.ActiveListItem
|
||||
import com.intellij.icons.AllIcons
|
||||
|
||||
internal const val HISTORY_DELETE_CELL = "delete"
|
||||
|
||||
internal data class LocalHistoryRow(
|
||||
val item: LocalHistoryItem,
|
||||
private val text: String,
|
||||
private val kind: SessionActivityKind?,
|
||||
override val section: String?,
|
||||
override val deleting: Boolean,
|
||||
) : ActiveListItem {
|
||||
override val key: String get() = item.id
|
||||
override val title: String get() = text
|
||||
override val trailing: String get() = HistoryTime.relative(item)
|
||||
override val search: String get() = listOfNotNull(text, item.id, item.directory).joinToString(" ")
|
||||
override val badges: List<ActiveListBadge>
|
||||
get() {
|
||||
if (deleting) return emptyList()
|
||||
return listOfNotNull(kind?.let { ActiveListBadge(it.label(), it.style()) })
|
||||
}
|
||||
override val cells: List<ActiveListCell>
|
||||
get() {
|
||||
if (deleting) return emptyList()
|
||||
return listOf(
|
||||
ActiveListCell(
|
||||
HISTORY_DELETE_CELL,
|
||||
KiloBundle.message("common.delete"),
|
||||
icon = AllIcons.Actions.GC,
|
||||
iconOnly = true,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal data class CloudHistoryRow(
|
||||
val item: CloudHistoryItem,
|
||||
private val text: String,
|
||||
private val kind: SessionActivityKind?,
|
||||
override val section: String?,
|
||||
) : ActiveListItem {
|
||||
override val key: String get() = item.id
|
||||
override val title: String get() = text
|
||||
override val trailing: String get() = HistoryTime.relative(item)
|
||||
override val search: String get() = listOf(text, item.id).joinToString(" ")
|
||||
override val badges: List<ActiveListBadge>
|
||||
get() = listOfNotNull(kind?.let { ActiveListBadge(it.label(), it.style()) })
|
||||
}
|
||||
|
||||
internal fun localHistoryRows(
|
||||
items: List<LocalHistoryItem>,
|
||||
snapshot: HistoryActivitySnapshot,
|
||||
deleting: (LocalHistoryItem) -> Boolean,
|
||||
): List<LocalHistoryRow> {
|
||||
return items.mapIndexed { idx, item ->
|
||||
LocalHistoryRow(
|
||||
item = item,
|
||||
text = snapshot.titles[item.id] ?: title(item),
|
||||
kind = snapshot.activity[item.id],
|
||||
section = historySection(items, idx),
|
||||
deleting = deleting(item),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
internal fun cloudHistoryRows(
|
||||
items: List<CloudHistoryItem>,
|
||||
snapshot: HistoryActivitySnapshot,
|
||||
): List<CloudHistoryRow> {
|
||||
return items.mapIndexed { idx, item ->
|
||||
CloudHistoryRow(
|
||||
item = item,
|
||||
text = title(item),
|
||||
kind = snapshot.activity[item.id],
|
||||
section = historySection(items, idx),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun historySection(items: List<HistoryItem>, idx: Int): String? {
|
||||
val item = items.getOrNull(idx) ?: return null
|
||||
val cur = HistoryTime.section(item)
|
||||
val prev = items.getOrNull(idx - 1)?.let(HistoryTime::section)
|
||||
if (cur == prev) return null
|
||||
return HistoryTime.title(cur)
|
||||
}
|
||||
+22
-1
@@ -1,9 +1,12 @@
|
||||
package ai.kilocode.client.ui.list
|
||||
|
||||
import ai.kilocode.client.ui.UiStyle
|
||||
import com.intellij.openapi.actionSystem.ActionGroup
|
||||
import com.intellij.openapi.actionSystem.ActionPlaces
|
||||
import com.intellij.openapi.options.advanced.AdvancedSettings
|
||||
import com.intellij.openapi.ui.popup.Balloon
|
||||
import com.intellij.ui.DocumentAdapter
|
||||
import com.intellij.ui.PopupHandler
|
||||
import com.intellij.ui.SearchTextField
|
||||
import com.intellij.ui.awt.RelativePoint
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
@@ -12,6 +15,7 @@ import com.intellij.util.ui.JBUI
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import com.intellij.util.ui.components.BorderLayoutPanel
|
||||
import java.awt.Color
|
||||
import java.awt.Cursor
|
||||
import java.awt.event.KeyEvent
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.KeyStroke
|
||||
@@ -36,11 +40,12 @@ internal class ActiveList(
|
||||
onOpen: ((ActiveListItem, Boolean) -> Unit)? = null,
|
||||
matcher: (String, ActiveListItem) -> Boolean = ::activeListMatches,
|
||||
enter: () -> Boolean = ::activeListEnterFocus,
|
||||
openOnClick: Boolean = true,
|
||||
onActivate: ((ActiveListItem) -> Unit)? = null,
|
||||
onClick: ((ActiveListItem) -> Unit)? = null,
|
||||
onSelect: (() -> Unit)? = null,
|
||||
) : BorderLayoutPanel() {
|
||||
private val view = ActiveListView(emptyText, cfg, surface, matcher, enter, onOpen, onActivate, onClick, onCell)
|
||||
private val view = ActiveListView(emptyText, cfg, surface, matcher, enter, openOnClick, onOpen, onActivate, onClick, onCell)
|
||||
private val search: SearchTextField? = if (showSearch) SearchTextField(false) else null
|
||||
private val scroll = object : JBScrollPane(view) {
|
||||
override fun getBackground(): Color {
|
||||
@@ -109,6 +114,22 @@ internal class ActiveList(
|
||||
@RequiresEdt
|
||||
fun focusList() = view.focusList()
|
||||
|
||||
@RequiresEdt
|
||||
fun setEmptyText(text: String) = view.setEmptyText(text)
|
||||
|
||||
@RequiresEdt
|
||||
fun installPopup(group: ActionGroup) = PopupHandler.installPopupMenu(view.list, group, ActionPlaces.POPUP)
|
||||
|
||||
@RequiresEdt
|
||||
fun setListCursor(cursor: Cursor) {
|
||||
view.list.cursor = cursor
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun setSelectionIndices(indices: IntArray) {
|
||||
view.list.selectedIndices = indices
|
||||
}
|
||||
|
||||
@RequiresEdt
|
||||
fun preferredFocus(): JComponent = view.list
|
||||
|
||||
|
||||
+6
@@ -40,6 +40,7 @@ internal class ActiveListView(
|
||||
private val surface: ActiveListSurface = ActiveListSurface.Default,
|
||||
private val matcher: (String, ActiveListItem) -> Boolean = ::activeListMatches,
|
||||
private val enter: () -> Boolean = ::activeListEnterFocus,
|
||||
private val openOnClick: Boolean = true,
|
||||
private val onOpen: ((ActiveListItem, Boolean) -> Unit)? = null,
|
||||
private val onActivate: ((ActiveListItem) -> Unit)? = null,
|
||||
private val onClick: ((ActiveListItem) -> Unit)? = null,
|
||||
@@ -126,6 +127,11 @@ internal class ActiveListView(
|
||||
val hit = hit(e, enabled = false) ?: return
|
||||
if (hit.id != null) return
|
||||
if (hit.item.deleting) return
|
||||
if (!openOnClick) {
|
||||
onClick?.invoke(hit.item) ?: return
|
||||
e.consume()
|
||||
return
|
||||
}
|
||||
val action = onOpen
|
||||
if (action != null) action(hit.item, false) else onClick?.invoke(hit.item) ?: return
|
||||
e.consume()
|
||||
|
||||
+41
-46
@@ -24,15 +24,11 @@ import com.intellij.openapi.application.ApplicationManager
|
||||
import com.intellij.openapi.util.Disposer
|
||||
import com.intellij.testFramework.fixtures.BasePlatformTestCase
|
||||
import com.intellij.ui.SearchTextField
|
||||
import com.intellij.ui.SimpleColoredComponent
|
||||
import com.intellij.ui.components.JBLabel
|
||||
import com.intellij.ui.components.JBList
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import com.intellij.util.ui.UIUtil
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import java.awt.BorderLayout
|
||||
import java.awt.Cursor
|
||||
import java.awt.event.KeyEvent
|
||||
import java.time.Instant
|
||||
@@ -42,6 +38,7 @@ import java.time.temporal.ChronoUnit
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import javax.swing.JComponent
|
||||
import javax.swing.KeyStroke
|
||||
import javax.swing.Scrollable
|
||||
import javax.swing.ScrollPaneConstants
|
||||
import javax.swing.event.ListDataEvent
|
||||
import javax.swing.event.ListDataListener
|
||||
@@ -176,61 +173,51 @@ class HistoryControllerTest : BasePlatformTestCase() {
|
||||
assertEquals(mapOf("ses_1" to SessionActivityKind.RUNNING), activity)
|
||||
}
|
||||
|
||||
fun `test local history renderer shows running badge for active id`() {
|
||||
fun `test local history rows show running badge for active id`() {
|
||||
val item = LocalHistoryItem(session("ses_1", "Running"))
|
||||
val controller = controller()
|
||||
controller.local.replace(listOf(item))
|
||||
val renderer = LocalHistoryRenderer(controller.local, activity = { mapOf("ses_1" to SessionActivityKind.RUNNING) })
|
||||
val snapshot = HistoryActivitySnapshot(activity = mapOf("ses_1" to SessionActivityKind.RUNNING))
|
||||
|
||||
renderer.getListCellRendererComponent(javax.swing.JList(arrayOf(item)), item, 0, false, false)
|
||||
val row = localHistoryRows(listOf(item), snapshot) { false }[0]
|
||||
|
||||
assertTrue(renderer.runningVisible())
|
||||
assertEquals(KiloBundle.message("session.part.tool.running"), row.badges.single().text)
|
||||
}
|
||||
|
||||
fun `test local history renderer uses title overlay`() {
|
||||
fun `test local history rows use title overlay`() {
|
||||
val item = LocalHistoryItem(session("ses_1", "Stored"))
|
||||
val controller = controller()
|
||||
controller.local.replace(listOf(item))
|
||||
val renderer = LocalHistoryRenderer(controller.local, titles = { mapOf("ses_1" to "Live") })
|
||||
val snapshot = HistoryActivitySnapshot(titles = mapOf("ses_1" to "Live"))
|
||||
|
||||
renderer.getListCellRendererComponent(javax.swing.JList(arrayOf(item)), item, 0, false, false)
|
||||
val row = localHistoryRows(listOf(item), snapshot) { false }[0]
|
||||
|
||||
assertEquals("Live", renderer.titleText())
|
||||
assertEquals("Live", row.title)
|
||||
}
|
||||
|
||||
fun `test cloud history renderer hides running badge for inactive id`() {
|
||||
fun `test cloud history rows hide running badge for inactive id`() {
|
||||
val item = CloudHistoryItem(cloud("cloud_1", "Cloud"))
|
||||
val controller = controller()
|
||||
controller.cloud.replace(listOf(item), null)
|
||||
val renderer = CloudHistoryRenderer(controller.cloud) { emptyMap() }
|
||||
|
||||
renderer.getListCellRendererComponent(javax.swing.JList(arrayOf(item)), item, 0, false, false)
|
||||
val row = cloudHistoryRows(listOf(item), HistoryActivitySnapshot())[0]
|
||||
|
||||
assertFalse(renderer.runningVisible())
|
||||
assertTrue(row.badges.isEmpty())
|
||||
}
|
||||
|
||||
fun `test history renderer uses trailing time with squeezable title`() {
|
||||
fun `test local history rows use trailing time and delete cell`() {
|
||||
val item = LocalHistoryItem(session("ses_1", "Long ".repeat(80)))
|
||||
val controller = controller()
|
||||
controller.local.replace(listOf(item))
|
||||
val renderer = LocalHistoryRenderer(controller.local)
|
||||
val view = renderer.getListCellRendererComponent(javax.swing.JList(arrayOf(item)), item, 0, false, false)
|
||||
val row = localHistoryRows(listOf(item), HistoryActivitySnapshot()) { false }[0]
|
||||
val cell = row.cells.single()
|
||||
|
||||
val title = UIUtil.uiTraverser(view).filter(SimpleColoredComponent::class.java).firstOrNull() ?: error("missing title")
|
||||
val head = title.parent
|
||||
assertTrue(head.layout is BorderLayout)
|
||||
assertSame(title, (head.layout as BorderLayout).getLayoutComponent(BorderLayout.CENTER))
|
||||
assertEquals(HistoryTime.relative(item), row.trailing)
|
||||
assertEquals(HISTORY_DELETE_CELL, cell.id)
|
||||
assertTrue(cell.iconOnly)
|
||||
}
|
||||
|
||||
val time = UIUtil.uiTraverser(view).filter(JBLabel::class.java)
|
||||
.first { it.text == HistoryTime.relative(item) }
|
||||
val main = time.parent
|
||||
assertTrue(main.layout is BorderLayout)
|
||||
assertSame(time, (main.layout as BorderLayout).getLayoutComponent(BorderLayout.EAST))
|
||||
fun `test deleting local history rows hide actions and badges`() {
|
||||
val item = LocalHistoryItem(session("ses_1", "Running"))
|
||||
val snapshot = HistoryActivitySnapshot(activity = mapOf("ses_1" to SessionActivityKind.RUNNING))
|
||||
|
||||
val row = main.parent as JComponent
|
||||
val ins = row.border.getBorderInsets(row)
|
||||
assertEquals(ins.left, ins.right)
|
||||
assertEquals(UiStyle.Gap.lg(), ins.right)
|
||||
val row = localHistoryRows(listOf(item), snapshot) { true }[0]
|
||||
|
||||
assertTrue(row.deleting)
|
||||
assertTrue(row.cells.isEmpty())
|
||||
assertTrue(row.badges.isEmpty())
|
||||
}
|
||||
|
||||
fun `test history panel sync updates running badges`() {
|
||||
@@ -921,17 +908,25 @@ class HistoryControllerTest : BasePlatformTestCase() {
|
||||
assertTrue(searches.all { inset(it.parent as JComponent) == UiStyle.Gap.lg() })
|
||||
|
||||
val scrolls = UIUtil.uiTraverser(panel.component).filter(JBScrollPane::class.java).toList()
|
||||
.filter { it.viewport.view is JBList<*> }
|
||||
.filter { it.viewport.view is Scrollable }
|
||||
assertTrue(scrolls.isNotEmpty())
|
||||
assertTrue(scrolls.all { it.horizontalScrollBarPolicy == ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER })
|
||||
assertTrue(scrolls.all { it.viewportBorder.getBorderInsets(it).right == UiStyle.Gap.lg() })
|
||||
assertTrue(scrolls.all { padded(it as JComponent) })
|
||||
|
||||
val lists = UIUtil.uiTraverser(panel.component).filter(JBList::class.java).toList()
|
||||
assertTrue(lists.isNotEmpty())
|
||||
assertTrue(lists.all { it.getScrollableTracksViewportWidth() })
|
||||
val views = scrolls.map { it.viewport.view as Scrollable }
|
||||
assertTrue(views.all { it.getScrollableTracksViewportWidth() })
|
||||
}
|
||||
|
||||
private fun inset(component: JComponent) = component.border.getBorderInsets(component).right
|
||||
private fun inset(component: JComponent) = component.border?.getBorderInsets(component)?.right ?: -1
|
||||
|
||||
private fun padded(component: JComponent): Boolean {
|
||||
var cur: java.awt.Component? = component
|
||||
while (cur != null) {
|
||||
if (cur is JComponent && inset(cur) == UiStyle.Gap.lg()) return true
|
||||
cur = cur.parent
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun collect(controller: HistoryController): MutableList<String> {
|
||||
val events = mutableListOf<String>()
|
||||
|
||||
Reference in New Issue
Block a user