mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
IJPL-171417 Go to target (choose implementation) freezes if there are many results
- Support FilteringModels in the cache - Added tests GitOrigin-RevId: 6791e4418c8481a0d6a2ea2b0341323d2a1e2da5
This commit is contained in:
committed by
intellij-monorepo-bot
parent
1f9763a7b3
commit
a60b64fe8a
@@ -825,7 +825,6 @@ com.intellij.ui.components.Magnificator
|
||||
- sf:CLIENT_PROPERTY_KEY:com.intellij.openapi.util.Key
|
||||
- a:magnify(D,java.awt.Point):java.awt.Point
|
||||
f:com.intellij.ui.components.WideSelectionListUI
|
||||
- uninstallUI(javax.swing.JComponent):V
|
||||
com.intellij.ui.components.ZoomableViewport
|
||||
- a:getMagnificator():com.intellij.ui.components.Magnificator
|
||||
- a:magnificationFinished(D):V
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.ui.components
|
||||
|
||||
import com.intellij.ui.speedSearch.FilteringListModel
|
||||
import org.jetbrains.annotations.ApiStatus
|
||||
import org.jetbrains.annotations.TestOnly
|
||||
import java.awt.Dimension
|
||||
import java.beans.PropertyChangeListener
|
||||
import java.util.*
|
||||
import javax.swing.JList
|
||||
import javax.swing.ListModel
|
||||
import javax.swing.event.ListDataEvent
|
||||
import javax.swing.event.ListDataListener
|
||||
|
||||
@ApiStatus.Internal
|
||||
@TestOnly
|
||||
class WideSelectionListCache(private val list: JList<*>) {
|
||||
|
||||
/**
|
||||
* Key is a value from the [list].
|
||||
* Don't cache `null` value because of using [MutableMap.getOrPut]
|
||||
*/
|
||||
@ApiStatus.Internal
|
||||
@TestOnly
|
||||
val preferredSizeCache: MutableMap<Any, Dimension> = IdentityHashMap()
|
||||
|
||||
private val listDataListener = object : ListDataListener {
|
||||
override fun intervalAdded(e: ListDataEvent?) {}
|
||||
|
||||
override fun intervalRemoved(e: ListDataEvent) {
|
||||
purge()
|
||||
}
|
||||
|
||||
override fun contentsChanged(e: ListDataEvent) {
|
||||
purge()
|
||||
}
|
||||
}
|
||||
|
||||
private val modelChangeListener = PropertyChangeListener { evt ->
|
||||
uninstallListDataListener(evt.oldValue as ListModel<*>?)
|
||||
installListDataListener(evt.newValue as ListModel<*>?)
|
||||
preferredSizeCache.clear()
|
||||
}
|
||||
|
||||
fun installListeners() {
|
||||
list.addPropertyChangeListener("model", modelChangeListener)
|
||||
installListDataListener(list.model)
|
||||
}
|
||||
|
||||
fun uninstallListeners() {
|
||||
list.removePropertyChangeListener("model", modelChangeListener)
|
||||
uninstallListDataListener(list.model)
|
||||
preferredSizeCache.clear()
|
||||
}
|
||||
|
||||
fun getCachedPreferredSizeOrCalculate(value: Any?, useCache: Boolean, supplier: () -> Dimension): Dimension {
|
||||
if (!useCache || value == null) {
|
||||
return supplier()
|
||||
}
|
||||
return preferredSizeCache.getOrPut(value, supplier)
|
||||
}
|
||||
|
||||
private fun purge() {
|
||||
val model = list.model.originalModel
|
||||
|
||||
if (model == null) {
|
||||
preferredSizeCache.clear()
|
||||
return
|
||||
}
|
||||
|
||||
val existingValues = Collections.newSetFromMap(IdentityHashMap<Any?, Boolean>())
|
||||
for (i in 0 until model.size) {
|
||||
existingValues.add(model.getElementAt(i))
|
||||
}
|
||||
preferredSizeCache.keys.retainAll(existingValues)
|
||||
}
|
||||
|
||||
private fun installListDataListener(model: ListModel<*>?) {
|
||||
model.originalModel?.addListDataListener(listDataListener)
|
||||
}
|
||||
|
||||
private fun uninstallListDataListener(model: ListModel<*>?) {
|
||||
model.originalModel?.removeListDataListener(listDataListener)
|
||||
}
|
||||
|
||||
private val ListModel<*>?.originalModel: ListModel<*>?
|
||||
get() {
|
||||
return when (this) {
|
||||
null -> null
|
||||
is FilteringListModel<*> -> originalModel
|
||||
else -> this
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,18 +10,17 @@ import com.intellij.ui.hover.ListHoverListener;
|
||||
import com.intellij.ui.list.ListCellBackgroundSupplier;
|
||||
import com.intellij.ui.render.RenderingUtil;
|
||||
import com.intellij.util.ui.UIUtil;
|
||||
import org.jetbrains.annotations.ApiStatus;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.TestOnly;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.ListDataEvent;
|
||||
import javax.swing.event.ListDataListener;
|
||||
import javax.swing.plaf.ComponentUI;
|
||||
import javax.swing.plaf.basic.BasicListUI;
|
||||
import java.awt.*;
|
||||
import java.awt.event.FocusEvent;
|
||||
import java.awt.event.FocusListener;
|
||||
import java.util.HashMap;
|
||||
|
||||
import static com.intellij.openapi.util.SystemInfo.isMac;
|
||||
import static com.intellij.ui.paint.RectanglePainter.DRAW;
|
||||
@@ -32,7 +31,10 @@ import static com.intellij.ui.paint.RectanglePainter.DRAW;
|
||||
public final class WideSelectionListUI extends BasicListUI {
|
||||
private static final Logger LOG = Logger.getInstance(WideSelectionListUI.class);
|
||||
private Rectangle myPaintBounds;
|
||||
private HashMap<@NotNull Integer, @NotNull Dimension> preferredSizeCache = new HashMap<>();
|
||||
|
||||
@ApiStatus.Internal
|
||||
@TestOnly
|
||||
public WideSelectionListCache cacheSupport;
|
||||
|
||||
@Override
|
||||
public void paint(Graphics g, JComponent c) {
|
||||
@@ -176,39 +178,17 @@ public final class WideSelectionListUI extends BasicListUI {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uninstallUI(JComponent c) {
|
||||
preferredSizeCache.clear();
|
||||
super.uninstallUI(c);
|
||||
protected void installListeners() {
|
||||
super.installListeners();
|
||||
cacheSupport = new WideSelectionListCache(list);
|
||||
cacheSupport.installListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ListDataListener createListDataListener() {
|
||||
ListDataListener superListener = super.createListDataListener();
|
||||
|
||||
return new ListDataListener() {
|
||||
@Override
|
||||
public void intervalAdded(ListDataEvent e) {
|
||||
superListener.intervalAdded(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void intervalRemoved(ListDataEvent e) {
|
||||
removeFromCache(e);
|
||||
superListener.intervalRemoved(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void contentsChanged(ListDataEvent e) {
|
||||
removeFromCache(e);
|
||||
superListener.contentsChanged(e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void removeFromCache(ListDataEvent e) {
|
||||
for (int i = e.getIndex0(); i <= e.getIndex1(); i++) {
|
||||
preferredSizeCache.remove(i);
|
||||
}
|
||||
protected void uninstallListeners() {
|
||||
cacheSupport.uninstallListeners();
|
||||
cacheSupport = null;
|
||||
super.uninstallListeners();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -264,25 +244,17 @@ public final class WideSelectionListUI extends BasicListUI {
|
||||
@NotNull ListModel<Object> dataModel,
|
||||
@NotNull ListCellRenderer<Object> renderer,
|
||||
boolean immutableRenderer) {
|
||||
if (immutableRenderer) {
|
||||
Dimension result = preferredSizeCache.get(index);
|
||||
if (result != null) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
Object value = dataModel.getElementAt(index);
|
||||
Component c = renderer.getListCellRendererComponent(list, value, index, false, false);
|
||||
rendererPane.add(c);
|
||||
var result = c.getPreferredSize();
|
||||
if (ClientProperty.get(c, JBList.IGNORE_LIST_ROW_HEIGHT) == null) {
|
||||
result = UIUtil.updateListRowHeight(result);
|
||||
}
|
||||
|
||||
if (immutableRenderer) {
|
||||
preferredSizeCache.put(index, result);
|
||||
}
|
||||
return result;
|
||||
return cacheSupport.getCachedPreferredSizeOrCalculate(value, immutableRenderer, () -> {
|
||||
Component c = renderer.getListCellRendererComponent(list, value, index, false, false);
|
||||
rendererPane.add(c);
|
||||
var result = c.getPreferredSize();
|
||||
if (ClientProperty.get(c, JBList.IGNORE_LIST_ROW_HEIGHT) == null) {
|
||||
result = UIUtil.updateListRowHeight(result);
|
||||
}
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+18
-6
@@ -9,12 +9,15 @@ import com.intellij.openapi.Disposable
|
||||
import com.intellij.ui.ColoredListCellRenderer
|
||||
import com.intellij.ui.SimpleTextAttributes
|
||||
import com.intellij.ui.components.JBList
|
||||
import com.intellij.ui.components.JBScrollPane
|
||||
import com.intellij.ui.components.WideSelectionListUI
|
||||
import com.intellij.ui.dsl.builder.Align
|
||||
import com.intellij.ui.dsl.builder.panel
|
||||
import com.intellij.ui.dsl.builder.text
|
||||
import com.intellij.ui.dsl.listCellRenderer.LcrInitParams
|
||||
import com.intellij.ui.dsl.listCellRenderer.listCellRenderer
|
||||
import com.intellij.ui.speedSearch.ListWithFilter
|
||||
import com.intellij.ui.speedSearch.NameFilteringListModel
|
||||
import com.intellij.util.ReflectionUtil
|
||||
import org.jetbrains.annotations.Nls
|
||||
import javax.swing.*
|
||||
@@ -30,13 +33,12 @@ internal class LcrPerformanceTestPanel : UISandboxPanel {
|
||||
override val title: String = "Performance"
|
||||
|
||||
override fun createContent(disposable: Disposable): JComponent {
|
||||
lateinit var list: JBList<Int>
|
||||
val list: JBList<Int> = JBList(DefaultListModel())
|
||||
return panel {
|
||||
row {
|
||||
list = scrollCell(JBList(DefaultListModel<Int>()))
|
||||
cell(ListWithFilter.wrap(list, JBScrollPane(list)) { getMainText(it) })
|
||||
.align(Align.FILL)
|
||||
.resizableColumn()
|
||||
.component
|
||||
applyRenderer(list, RendererType.KOTLIN_UI_DSL)
|
||||
|
||||
panel {
|
||||
@@ -45,10 +47,10 @@ internal class LcrPerformanceTestPanel : UISandboxPanel {
|
||||
.text("10000")
|
||||
.component
|
||||
button("Generate") {
|
||||
val model = list.model as DefaultListModel<Int>
|
||||
val model = list.model as NameFilteringListModel<Int>
|
||||
val count = tfCount.text.toIntOrNull() ?: return@button
|
||||
val size = model.size
|
||||
model.addAll(size, (size..<size + count).toList())
|
||||
model.addAll((size..<size + count).toList())
|
||||
list.invalidate()
|
||||
}
|
||||
}
|
||||
@@ -62,6 +64,14 @@ internal class LcrPerformanceTestPanel : UISandboxPanel {
|
||||
}
|
||||
}
|
||||
|
||||
row {
|
||||
checkBox("ImmutableModelAndRenderer")
|
||||
.comment("Turn on performance optimizations")
|
||||
.onChanged {
|
||||
list.putClientProperty(JBList.IMMUTABLE_MODEL_AND_RENDERER, it.isSelected)
|
||||
}
|
||||
}
|
||||
|
||||
row {
|
||||
lateinit var label: JLabel
|
||||
button("updateLayoutState") {
|
||||
@@ -83,7 +93,9 @@ internal class LcrPerformanceTestPanel : UISandboxPanel {
|
||||
list.cellRenderer = when (renderer) {
|
||||
RendererType.KOTLIN_UI_DSL -> listCellRenderer("") {
|
||||
icon(getMainIcon(value))
|
||||
text(getMainText(value))
|
||||
text(getMainText(value)) {
|
||||
speedSearch { }
|
||||
}
|
||||
text(getSecondaryText(value)) {
|
||||
foreground = greyForeground
|
||||
}
|
||||
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
// Copyright 2000-2025 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
|
||||
package com.intellij.ui.components
|
||||
|
||||
import com.intellij.ui.speedSearch.FilteringListModel
|
||||
import org.junit.Test
|
||||
import javax.swing.DefaultListModel
|
||||
import javax.swing.ListModel
|
||||
import kotlin.test.assertEquals
|
||||
|
||||
class WideSelectionListCacheTest {
|
||||
|
||||
@Test
|
||||
fun testNoOptimizationDefaultModel() {
|
||||
val list = JBList((0..100).toList())
|
||||
val model = list.model as DefaultListModel<Int>
|
||||
|
||||
updateAndCheckCache(list, emptySet())
|
||||
model.remove(10)
|
||||
updateAndCheckCache(list, emptySet())
|
||||
model.addElement(101)
|
||||
updateAndCheckCache(list, emptySet())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOptimizationDefaultModel() {
|
||||
val list = JBList((0..100).toList())
|
||||
val model = list.model as DefaultListModel<Int>
|
||||
|
||||
list.putClientProperty(JBList.IMMUTABLE_MODEL_AND_RENDERER, true)
|
||||
|
||||
updateAndCheckCacheEqualsModel(list)
|
||||
model.remove(10)
|
||||
updateAndCheckCacheEqualsModel(list)
|
||||
model.addElement(101)
|
||||
updateAndCheckCacheEqualsModel(list)
|
||||
|
||||
list.model = DefaultListModel()
|
||||
assertEquals(0, list.cachedItems.size)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testFilteringModel() {
|
||||
val defaultModel = DefaultListModel<Int>()
|
||||
defaultModel.addAll((0..100).toList())
|
||||
val filteringModel = FilteringListModel(defaultModel)
|
||||
val list = JBList(filteringModel)
|
||||
|
||||
list.putClientProperty(JBList.IMMUTABLE_MODEL_AND_RENDERER, true)
|
||||
|
||||
filteringModel.setFilter { it <= 50 }
|
||||
updateAndCheckCache(list, (0..50).toSet())
|
||||
filteringModel.setFilter { it <= 60 }
|
||||
updateAndCheckCache(list, (0..60).toSet())
|
||||
filteringModel.setFilter(null)
|
||||
updateAndCheckCache(list, (0..100).toSet())
|
||||
filteringModel.setFilter { it <= 50 }
|
||||
updateAndCheckCache(list, (0..100).toSet())
|
||||
|
||||
filteringModel.setFilter(null)
|
||||
defaultModel.remove(50)
|
||||
val no50 = defaultModel.allItems
|
||||
assertEquals(no50, list.cachedItems)
|
||||
defaultModel.addElement(999)
|
||||
assertEquals(no50, list.cachedItems)
|
||||
updateAndCheckCacheEqualsModel(list)
|
||||
}
|
||||
|
||||
private val ListModel<Int>.allItems: Set<Int>
|
||||
get() = checkedToSet((0..<size).map { getElementAt(it) })
|
||||
|
||||
private val JBList<Int>.cachedItems: Set<Int>
|
||||
get() = checkedToSet((ui as WideSelectionListUI).cacheSupport.preferredSizeCache.keys)
|
||||
|
||||
private fun checkedToSet(list: Collection<Any>): Set<Int> {
|
||||
val result = list.filterIsInstance<Int>().toSet()
|
||||
assertEquals(result.size, list.size)
|
||||
return result
|
||||
}
|
||||
|
||||
private fun updateAndCheckCache(list: JBList<Int>, items: Set<Any>) {
|
||||
val ui = list.ui as WideSelectionListUI
|
||||
ui.updateLayoutState()
|
||||
|
||||
assertEquals(list.cachedItems, items.toSet())
|
||||
}
|
||||
|
||||
private fun updateAndCheckCacheEqualsModel(list: JBList<Int>) {
|
||||
updateAndCheckCache(list, list.model.allItems)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user