From d738bf447faa3ea0a9bb2597d9809d327998b6d8 Mon Sep 17 00:00:00 2001 From: Anton Efimchuk Date: Tue, 18 Jun 2024 17:10:27 +0200 Subject: [PATCH] PY-73345 Optimize cell bounds calculations GitOrigin-RevId: a65412190555d20e08a54d6717e7224912611230 --- .../visualization/NotebookCellInlayManager.kt | 74 ++++++------- .../ui/ControllerEditorCellViewComponent.kt | 39 ++----- .../visualization/ui/DecoratedEditor.kt | 13 ++- .../notebooks/visualization/ui/EditorCell.kt | 4 - .../visualization/ui/EditorCellInput.kt | 90 ++++++--------- .../visualization/ui/EditorCellOutput.kt | 42 ++++--- .../visualization/ui/EditorCellOutputs.kt | 52 ++++----- .../visualization/ui/EditorCellView.kt | 104 +++++++++--------- .../ui/EditorCellViewComponent.kt | 70 ++++++++++-- .../ui/EditorCellViewComponentListener.kt | 11 -- .../visualization/ui/HasGutterIcon.kt | 8 ++ .../ui/TextEditorCellViewComponent.kt | 44 +++----- 12 files changed, 256 insertions(+), 295 deletions(-) delete mode 100644 notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/EditorCellViewComponentListener.kt create mode 100644 notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/HasGutterIcon.kt diff --git a/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/NotebookCellInlayManager.kt b/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/NotebookCellInlayManager.kt index 0f92d84d7961..a5f9e7d45306 100644 --- a/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/NotebookCellInlayManager.kt +++ b/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/NotebookCellInlayManager.kt @@ -1,11 +1,8 @@ package org.jetbrains.plugins.notebooks.visualization -import com.intellij.codeInsight.codeVision.ui.popup.layouter.bottom -import com.intellij.codeInsight.codeVision.ui.popup.layouter.right import com.intellij.ide.ui.LafManagerListener import com.intellij.openapi.Disposable import com.intellij.openapi.application.ApplicationManager -import com.intellij.openapi.application.runInEdt import com.intellij.openapi.editor.Editor import com.intellij.openapi.editor.EditorKind import com.intellij.openapi.editor.FoldRegion @@ -36,16 +33,14 @@ import org.jetbrains.plugins.notebooks.visualization.ui.EditorCellEventListener. import org.jetbrains.plugins.notebooks.visualization.ui.EditorCellView import org.jetbrains.plugins.notebooks.visualization.ui.keepScrollingPositionWhile import java.awt.Graphics -import java.awt.Point import java.util.* import kotlin.math.max import kotlin.math.min class NotebookCellInlayManager private constructor( - val editor: EditorImpl + val editor: EditorImpl, ) : Disposable, NotebookIntervalPointerFactory.ChangeListener { private val notebookCellLines = NotebookCellLines.get(editor) - private val viewportQueue = MergingUpdateQueue("NotebookCellInlayManager Viewport Update", 100, true, null, editor.disposable, null, true) /** 20 is 1000 / 50, two times faster than the eye refresh rate. Actually, the value has been chosen randomly, without experiments. */ private val updateQueue = MergingUpdateQueue("NotebookCellInlayManager Interval Update", 20, true, null, editor.disposable, null, true) @@ -62,6 +57,8 @@ class NotebookCellInlayManager private constructor( private val cellEventListeners = EventDispatcher.create(EditorCellEventListener::class.java) + private var valid = false + override fun dispose() {} fun getCellForInterval(interval: NotebookCellLines.Interval): EditorCell = @@ -100,18 +97,9 @@ class NotebookCellInlayManager private constructor( private fun addViewportChangeListener() { editor.scrollPane.viewport.addChangeListener { - val rect = editor.scrollPane.viewport.viewRect - val top = editor.xyToLogicalPosition(rect.location) - val bottom = editor.xyToLogicalPosition(Point(rect.right, rect.bottom)) - scheduleUpdatePositions(top.line, bottom.line) - viewportQueue.queue(object : Update("Viewport change") { - override fun run() { - if (editor.isDisposed) return - _cells.forEach { - it.onViewportChange() - } - } - }) + _cells.forEach { + it.onViewportChange() + } } } @@ -136,7 +124,7 @@ class NotebookCellInlayManager private constructor( editor.foldingModel.addListener(object : FoldingListener { override fun onFoldProcessingEnd() { - scheduleUpdatePositions() + invalidateCells() } }, editor.disposable) @@ -215,24 +203,6 @@ class NotebookCellInlayManager private constructor( startOffset >= region.startOffset && endOffset <= region.endOffset } - private fun scheduleUpdatePositions(from: Int = 0, to: Int = 1000_000_000) { - runInEdt { - val fromIndex = _cells.indexOfFirst { cell -> - val lines = cell.intervalPointer.get()?.lines - lines != null && lines.hasIntersectionWith(from..to + 1) - } - if (fromIndex == -1) return@runInEdt - val toIndex = _cells.subList(fromIndex, _cells.size).indexOfFirst { cell -> - val lines = cell.intervalPointer.get()?.lines - lines != null && !lines.hasIntersectionWith(from..to + 1) - } - val finalIndex = if (toIndex == -1) _cells.size - 1 else fromIndex + toIndex - for (i in max(fromIndex - 1, 0)..min(finalIndex + 1, _cells.size - 1)) { - _cells[i].updatePositions() - } - } - } - private fun refreshHighlightersLookAndFeel() { for (highlighter in editor.markupModel.allHighlighters) { if (highlighter.customRenderer === NotebookCellHighlighterRenderer) { @@ -257,7 +227,7 @@ class NotebookCellInlayManager private constructor( } private fun createCell(interval: NotebookIntervalPointer) = EditorCell(editor, interval) { cell -> - EditorCellView(editor, notebookCellLines, cell).also { Disposer.register(cell, it) } + EditorCellView(editor, notebookCellLines, cell, this).also { Disposer.register(cell, it) } }.also { Disposer.register(this, it) } private fun ensureInlaysAndHighlightersExist(matchingCellsBeforeChange: List, logicalLines: IntRange) { @@ -413,7 +383,7 @@ class NotebookCellInlayManager private constructor( } } if (needUpdatePositions) { - scheduleUpdatePositions() + invalidateCells() } cellEventListeners.multicaster.onEditorCellEvents(events) updateConsequentInlays(start..end) @@ -426,6 +396,22 @@ class NotebookCellInlayManager private constructor( fun getCell(index: Int): EditorCell { return cells[index] } + + fun invalidateCells() { + valid = false + } + + fun validateCells() { + if (!valid) { + _cells.forEach { + it.view?.also { view -> + view.bounds = view.calculateBounds() + view.validate() + } + } + valid = true + } + } } /** @@ -455,10 +441,12 @@ private object NotebookCellHighlighterRenderer : CustomHighlighterRenderer { } } -private class UpdateInlaysTask(private val manager: NotebookCellInlayManager, - pointers: Collection? = null, - private var updateAll: Boolean = false, - private var forceUpdate: Boolean = false) : Update(Any()) { +private class UpdateInlaysTask( + private val manager: NotebookCellInlayManager, + pointers: Collection? = null, + private var updateAll: Boolean = false, + private var forceUpdate: Boolean = false, +) : Update(Any()) { private val pointerSet = pointers?.let { SmartHashSet(pointers) } ?: SmartHashSet() override fun run() { diff --git a/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/ControllerEditorCellViewComponent.kt b/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/ControllerEditorCellViewComponent.kt index bcb7560471a9..11b8b9d75271 100644 --- a/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/ControllerEditorCellViewComponent.kt +++ b/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/ControllerEditorCellViewComponent.kt @@ -2,36 +2,21 @@ package org.jetbrains.plugins.notebooks.visualization.ui import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.util.Disposer -import com.intellij.util.EventDispatcher import com.intellij.util.asSafely import org.jetbrains.plugins.notebooks.visualization.NotebookCellInlayController -import java.awt.Dimension -import java.awt.Point +import java.awt.Rectangle import java.awt.event.ComponentAdapter import java.awt.event.ComponentEvent import javax.swing.JComponent class ControllerEditorCellViewComponent( - internal val controller: NotebookCellInlayController -) : EditorCellViewComponent { - - private val cellEventListeners = EventDispatcher.create(EditorCellViewComponentListener::class.java) - - override val location: Point - get() { - val component = controller.inlay.renderer as JComponent - return component.location - } - - override val size: Dimension - get() { - val component = controller.inlay.renderer as JComponent - return component.size - } + internal val controller: NotebookCellInlayController, + private val parent: EditorCellInput, +) : EditorCellViewComponent(), HasGutterIcon { private val listener = object : ComponentAdapter() { override fun componentResized(e: ComponentEvent) { - cellEventListeners.multicaster.componentBoundaryChanged(e.component.location, e.component.size) + parent.invalidate() } } @@ -45,21 +30,17 @@ class ControllerEditorCellViewComponent( inlay.update() } - override fun dispose() { + override fun doDispose() { controller.inlay.renderer.asSafely()?.removeComponentListener(listener) controller.let { controller -> Disposer.dispose(controller.inlay) } } - override fun onViewportChange() { + override fun doViewportChange() { controller.onViewportChange() } - - override fun addViewComponentListener(listener: EditorCellViewComponentListener) { - cellEventListeners.addListener(listener) - } - - override fun updatePositions() { - cellEventListeners.multicaster.componentBoundaryChanged(location, size) + override fun calculateBounds(): Rectangle { + val component = controller.inlay.renderer as JComponent + return component.bounds } } \ No newline at end of file diff --git a/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/DecoratedEditor.kt b/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/DecoratedEditor.kt index 7f3bd14bde31..19d92ccbd5f0 100644 --- a/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/DecoratedEditor.kt +++ b/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/DecoratedEditor.kt @@ -22,7 +22,7 @@ import javax.swing.JLayer import javax.swing.SwingUtilities import javax.swing.plaf.LayerUI -private class DecoratedEditor(private val original: TextEditor) : TextEditor by original { +private class DecoratedEditor(private val original: TextEditor, private val manager: NotebookCellInlayManager) : TextEditor by original { private var mouseOverCell: EditorCellView? = null @@ -64,6 +64,7 @@ private class DecoratedEditor(private val original: TextEditor) : TextEditor by override fun validateTree() { keepScrollingPositionWhile(editor) { + manager.validateCells() super.validateTree() } } @@ -118,12 +119,12 @@ private class DecoratedEditor(private val original: TextEditor) : TextEditor by }) private fun updateMouseOverCell(component: JComponent, point: Point) { - val cells = NotebookCellInlayManager.get(editor)!!.cells + val cells = manager.cells val currentOverCell = cells.filter { it.visible }.mapNotNull { it.view }.firstOrNull { val viewLeft = 0 - val viewTop = it.location.y + val viewTop = it.bounds.y val viewRight = component.size.width - val viewBottom = viewTop + it.size.height + val viewBottom = viewTop + it.bounds.height viewLeft <= point.x && viewTop <= point.y && viewRight >= point.x && viewBottom >= point.y } if (mouseOverCell != currentOverCell) { @@ -135,8 +136,8 @@ private class DecoratedEditor(private val original: TextEditor) : TextEditor by } -fun decorateTextEditor(textEditor: TextEditor): TextEditor { - return DecoratedEditor(textEditor) +fun decorateTextEditor(textEditor: TextEditor, manager: NotebookCellInlayManager): TextEditor { + return DecoratedEditor(textEditor, manager) } internal fun keepScrollingPositionWhile(editor: Editor, task: Runnable) { diff --git a/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/EditorCell.kt b/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/EditorCell.kt index 5c80ba81257e..e47264c2409d 100644 --- a/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/EditorCell.kt +++ b/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/EditorCell.kt @@ -68,10 +68,6 @@ class EditorCell( } } - fun updatePositions() { - view?.updatePositions() - } - override fun dispose() { view?.let { Disposer.dispose(it) } } diff --git a/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/EditorCellInput.kt b/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/EditorCellInput.kt index 63fca3e2a00b..fa62b811cb24 100644 --- a/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/EditorCellInput.kt +++ b/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/EditorCellInput.kt @@ -4,20 +4,15 @@ import com.intellij.openapi.actionSystem.AnAction import com.intellij.openapi.editor.FoldRegion import com.intellij.openapi.editor.ex.EditorEx import com.intellij.openapi.util.TextRange -import com.intellij.util.EventDispatcher import org.jetbrains.plugins.notebooks.ui.visualization.notebookAppearance import org.jetbrains.plugins.notebooks.visualization.NotebookCellLines -import java.awt.Dimension -import java.awt.Point import java.awt.Rectangle class EditorCellInput( private val editor: EditorEx, - private val componentFactory: (EditorCellViewComponent?) -> EditorCellViewComponent, - private val cell: EditorCell -) { - - private val cellEventListeners = EventDispatcher.create(EditorCellViewComponentListener::class.java) + private val componentFactory: (EditorCellInput, EditorCellViewComponent?) -> EditorCellViewComponent, + private val cell: EditorCell, +): EditorCellViewComponent() { val interval: NotebookCellLines.Interval get() = cell.intervalPointer.get() ?: error("Invalid interval") @@ -33,60 +28,31 @@ class EditorCellInput( else -> editor.notebookAppearance.cellBorderHeight / 2 } - val bounds: Rectangle - get() { - val linesRange = interval.lines - val startOffset = editor.document.getLineStartOffset(linesRange.first) - val endOffset = editor.document.getLineEndOffset(linesRange.last) - val bounds = editor.inlayModel.getBlockElementsInRange(startOffset, endOffset) - .asSequence() - .filter { it.properties.priority > editor.notebookAppearance.NOTEBOOK_OUTPUT_INLAY_PRIORITY } - .mapNotNull { it.bounds } - .fold(Rectangle(_component.location, _component.size)) { b, i -> - b.union(i) - } - return bounds - } - - private var _component: EditorCellViewComponent = componentFactory(null).also { bind(it) } + private var _component: EditorCellViewComponent = componentFactory(this, null) set(value) { if (value != field) { field.dispose() + remove(field) field = value - bind(value) + add(value) } } - private fun bind(value: EditorCellViewComponent) { - value.addViewComponentListener(object : EditorCellViewComponentListener { - override fun componentBoundaryChanged(location: Point, size: Dimension) { - cellEventListeners.multicaster.componentBoundaryChanged(bounds.location, bounds.size) - } - }) - } - val component: EditorCellViewComponent get() = _component private val folding: EditorCellFolding = EditorCellFolding(editor) { toggleFolding(componentFactory) - }.also { - cellEventListeners.addListener(object : EditorCellViewComponentListener { - override fun componentBoundaryChanged(location: Point, size: Dimension) { - it.updatePosition(location.y + delimiterPanelSize, size.height - delimiterPanelSize) - } - }) } - private fun toggleFolding(inputComponentFactory: (EditorCellViewComponent) -> EditorCellViewComponent) { + private fun toggleFolding(inputComponentFactory: (EditorCellInput, EditorCellViewComponent) -> EditorCellViewComponent) { _component = if (_component is ControllerEditorCellViewComponent) { - _component.dispose() toggleTextFolding() TextEditorCellViewComponent(editor, cell) } else { toggleTextFolding() - inputComponentFactory(_component) + inputComponentFactory(this, _component) } } @@ -116,7 +82,7 @@ class EditorCellInput( private var gutterAction: AnAction? = null - fun dispose() { + override fun doDispose() { folding.dispose() runCellButton?.dispose() _component.dispose() @@ -124,20 +90,16 @@ class EditorCellInput( fun update(force: Boolean = false) { val oldComponent = if (force) null else _component - _component = componentFactory(oldComponent) + _component = componentFactory(this, oldComponent) updateGutterIcons() } private fun updateGutterIcons() { - _component.updateGutterIcons(gutterAction) + (_component as? HasGutterIcon)?.updateGutterIcons(gutterAction) } - fun updatePositions() { - _component.updatePositions() - } - - fun onViewportChange() { - _component.onViewportChange() + override fun doLayout() { + folding.updatePosition(bounds.y + delimiterPanelSize, bounds.height - delimiterPanelSize) } fun setGutterAction(action: AnAction) { @@ -156,32 +118,44 @@ class EditorCellInput( fun showRunButton() { try { runCellButton?.showRunButton(interval) - } catch (e: IllegalStateException) { return } + } + catch (e: IllegalStateException) { + return + } } fun hideRunButton() { runCellButton?.hideRunButton() } - fun addViewComponentListener(listener: EditorCellViewComponentListener) { - cellEventListeners.addListener(listener) - } - fun updatePresentation(view: EditorCellViewComponent) { - _component.dispose() _component = view } fun updateSelection(value: Boolean) { folding.updateSelection(value) } + + override fun calculateBounds(): Rectangle { + val linesRange = interval.lines + val startOffset = editor.document.getLineStartOffset(linesRange.first) + val endOffset = editor.document.getLineEndOffset(linesRange.last) + val bounds = editor.inlayModel.getBlockElementsInRange(startOffset, endOffset) + .asSequence() + .filter { it.properties.priority > editor.notebookAppearance.NOTEBOOK_OUTPUT_INLAY_PRIORITY } + .mapNotNull { it.bounds } + .fold(_component.calculateBounds()) { b, i -> + b.union(i) + } + return bounds + } } private fun String.ellipsis(length: Int): String { return if (this.length > length) { substring(0, length - 1) } - else { + else { this } + "\u2026" } \ No newline at end of file diff --git a/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/EditorCellOutput.kt b/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/EditorCellOutput.kt index bbc6d27cfabb..96b37cc00eed 100644 --- a/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/EditorCellOutput.kt +++ b/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/EditorCellOutput.kt @@ -19,12 +19,12 @@ import javax.swing.SwingUtilities val NOTEBOOK_CELL_OUTPUT_DATA_KEY = DataKey.create("NOTEBOOK_CELL_OUTPUT") -class EditorCellOutput internal constructor(private val editor: EditorEx, private val component: CollapsingComponent, private val disposable: Disposable?) { +class EditorCellOutput internal constructor( + private val editor: EditorEx, + private val component: CollapsingComponent, + private val disposable: Disposable?, +) : EditorCellViewComponent() { - val location: Point - get() = SwingUtilities.convertPoint(component.parent, component.location, editor.contentComponent) - val size: Dimension - get() = component.size var collapsed: Boolean get() = !component.isSeen set(value) { @@ -38,11 +38,11 @@ class EditorCellOutput internal constructor(private val editor: EditorEx, privat .also { component.addComponentListener(object : ComponentAdapter() { override fun componentMoved(e: ComponentEvent) { - updatePositions() + invalidate() } override fun componentResized(e: ComponentEvent) { - updatePositions() + invalidate() } }) } @@ -58,21 +58,17 @@ class EditorCellOutput internal constructor(private val editor: EditorEx, privat } } - fun updatePositions() { - folding.updatePosition(location.y, size.height) - } - - fun dispose() { + override fun doDispose() { folding.dispose() disposable?.let { Disposer.dispose(it) } } - fun onViewportChange() { - val component = component.mainComponent as? NotebookOutputInlayShowable ?: return - if (component !is JComponent) return - validateComponent(component) - val componentRect = SwingUtilities.convertRectangle(component, component.bounds, editor.scrollPane.viewport.view) - component.shown = editor.scrollPane.viewport.viewRect.intersects(componentRect) + override fun doViewportChange() { + val component = component.mainComponent as? NotebookOutputInlayShowable ?: return + if (component !is JComponent) return + validateComponent(component) + val componentRect = SwingUtilities.convertRectangle(component, component.bounds, editor.scrollPane.viewport.view) + component.shown = editor.scrollPane.viewport.viewRect.intersects(componentRect) } fun hideFolding() { @@ -100,4 +96,14 @@ class EditorCellOutput internal constructor(private val editor: EditorEx, privat } } } + + override fun calculateBounds(): Rectangle { + val location = SwingUtilities.convertPoint(component.parent, component.location, editor.contentComponent) + val size = component.size + return Rectangle(location, size) + } + + override fun doLayout() { + folding.updatePosition(bounds.y, bounds.height) + } } \ No newline at end of file diff --git a/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/EditorCellOutputs.kt b/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/EditorCellOutputs.kt index c08c60226cd1..08c070faef74 100644 --- a/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/EditorCellOutputs.kt +++ b/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/EditorCellOutputs.kt @@ -8,7 +8,6 @@ import com.intellij.openapi.editor.Document import com.intellij.openapi.editor.Inlay import com.intellij.openapi.editor.impl.EditorImpl import com.intellij.openapi.util.Disposer -import com.intellij.util.EventDispatcher import com.intellij.util.asSafely import org.jetbrains.plugins.notebooks.ui.visualization.notebookAppearance import org.jetbrains.plugins.notebooks.visualization.NotebookCellLines @@ -30,12 +29,8 @@ import javax.swing.JComponent class EditorCellOutputs( private val editor: EditorImpl, private val interval: () -> NotebookCellLines.Interval, - private val onInlayDisposed: (EditorCellOutputs) -> Unit = {} -) : Disposable { - - - - private val cellEventListeners = EventDispatcher.create(EditorCellViewComponentListener::class.java) + private val onInlayDisposed: (EditorCellOutputs) -> Unit = {}, +) : EditorCellViewComponent(), Disposable { private val _outputs = mutableListOf() val outputs @@ -55,30 +50,17 @@ class EditorCellOutputs( } private var inlay: Inlay<*>? = null - val bounds: Rectangle? - get() { - return inlay?.bounds - } - init { update() } - override fun dispose() { + override fun doDispose() { outputs.forEach { it.dispose() } inlay?.let { Disposer.dispose(it) } } - fun updatePositions() { - val b = bounds - if (b != null) { - cellEventListeners.multicaster.componentBoundaryChanged(b.location, b.size) - outputs.forEach { it.updatePositions() } - } - } - - fun onViewportChange() { - outputs.forEach { it.onViewportChange() } + override fun calculateBounds(): Rectangle { + return inlay?.bounds ?: Rectangle(0, 0, 0, 0) } fun updateSelection(selected: Boolean) { @@ -172,7 +154,9 @@ class EditorCellOutputs( private fun removeOutput(idx: Int) { innerComponent.remove(idx) - _outputs.removeAt(idx).dispose() + val outputComponent = _outputs.removeAt(idx) + outputComponent.dispose() + remove(outputComponent) } private fun createOutputGuessingFactory(outputDataKey: K): NotebookOutputComponentFactory.CreatedComponent<*>? = @@ -185,8 +169,10 @@ class EditorCellOutputs( } .firstOrNull() - private fun createOutput(factory: NotebookOutputComponentFactory<*, K>, - outputDataKey: K): NotebookOutputComponentFactory.CreatedComponent<*>? { + private fun createOutput( + factory: NotebookOutputComponentFactory<*, K>, + outputDataKey: K, + ): NotebookOutputComponentFactory.CreatedComponent<*>? { val lines = interval().lines ApplicationManager.getApplication().messageBus.syncPublisher(OUTPUT_LISTENER).beforeOutputCreated(editor, lines.last) val result = try { @@ -238,7 +224,7 @@ class EditorCellOutputs( ).also { it.renderer.asSafely()?.addComponentListener(object : ComponentAdapter() { override fun componentResized(e: ComponentEvent) { - cellEventListeners.multicaster.componentBoundaryChanged(e.component.location, e.component.size) + invalidate() } }) Disposer.register(it) { @@ -263,7 +249,9 @@ class EditorCellOutputs( pos, ) - _outputs.add(if (pos == -1) _outputs.size else pos, EditorCellOutput(editor, collapsingComponent, newComponent.disposable)) + val outputComponent = EditorCellOutput(editor, collapsingComponent, newComponent.disposable) + _outputs.add(if (pos == -1) _outputs.size else pos, outputComponent) + add(outputComponent) // DS-1972 Without revalidation, the component would be just invalidated, and would be rendered only after anything else requests // for repainting the editor. @@ -275,9 +263,11 @@ class EditorCellOutputs( override fun next(): Pair = this@zip.next() to other.next() } - fun paintGutter(editor: EditorImpl, - g: Graphics, - r: Rectangle) { + fun paintGutter( + editor: EditorImpl, + g: Graphics, + r: Rectangle, + ) { val yOffset = innerComponent.yOffsetFromEditor(editor) ?: return val oldClip = g.clipBounds diff --git a/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/EditorCellView.kt b/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/EditorCellView.kt index 81761fd2d41a..ab46010efdae 100644 --- a/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/EditorCellView.kt +++ b/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/EditorCellView.kt @@ -36,8 +36,9 @@ import kotlin.reflect.KClass class EditorCellView( private val editor: EditorImpl, private val intervals: NotebookCellLines, - internal var cell: EditorCell -) : Disposable { + internal var cell: EditorCell, + private val cellInlayManager: NotebookCellInlayManager, +) : EditorCellViewComponent(), Disposable { private var _controllers: List = emptyList() private val controllers: List @@ -54,7 +55,7 @@ class EditorCellView( val input: EditorCellInput = EditorCellInput( editor, - { currentComponent: EditorCellViewComponent? -> + { parent: EditorCellInput, currentComponent: EditorCellViewComponent? -> val currentController = (currentComponent as? ControllerEditorCellViewComponent)?.controller val controller = getInputFactories().firstNotNullOfOrNull { factory -> failSafeCompute(factory, editor, currentController?.let { listOf(it) } @@ -65,27 +66,16 @@ class EditorCellView( currentComponent } else { - ControllerEditorCellViewComponent(controller) + ControllerEditorCellViewComponent(controller, parent) } } else { TextEditorCellViewComponent(editor, cell) } - }, cell).also { - it.addViewComponentListener(object : EditorCellViewComponentListener { - override fun componentBoundaryChanged(location: Point, size: Dimension) { - updateBoundaries() - } - }) - } - - private var _location: Point = Point(0, 0) - - val location: Point get() = _location - - private var _size: Dimension = Dimension(0, 0) - - val size: Dimension get() = _size + }, cell) + .also { + add(it) + } private var _outputs: EditorCellOutputs? = null @@ -101,23 +91,12 @@ class EditorCellView( updateSelection(false) } - private fun updateBoundaries() { - val inputBounds = input.bounds - val y = inputBounds.y - _location = Point(0, y) - val currentOutputs = outputs - _size = Dimension( - editor.contentSize.width, - currentOutputs?.bounds?.let { it.height + it.y - y } ?: inputBounds.height - ) - } - - override fun dispose() { + override fun doDispose() { _controllers.forEach { controller -> disposeController(controller) } input.dispose() - + outputs?.dispose() removeCellHighlight() } @@ -128,13 +107,8 @@ class EditorCellView( } fun update(force: Boolean = false) { - extracted(force) - } - - private fun extracted(force: Boolean) { val otherFactories = NotebookCellInlayController.Factory.EP_NAME.extensionList .filter { it !is NotebookCellInlayController.InputFactory } - val controllersToDispose = _controllers.toMutableSet() _controllers = if (!editor.isDisposed) { otherFactories.mapNotNull { factory -> failSafeCompute(factory, editor, _controllers, intervals.intervals.listIterator(interval.ordinal)) } @@ -157,14 +131,18 @@ class EditorCellView( } input.update(force) updateOutputs() - updateBoundaries() updateCellHighlight() + invalidate() } private fun updateOutputs() { if (hasOutputs()) { if (_outputs == null) { - _outputs = EditorCellOutputs(editor, { interval }).also { Disposer.register(this, it) } + _outputs = EditorCellOutputs(editor, { interval }) + .also { + Disposer.register(this, it) + add(it) + } updateCellHighlight() updateFolding() } @@ -173,7 +151,10 @@ class EditorCellView( } } else { - outputs?.let { Disposer.dispose(it) } + outputs?.let { + Disposer.dispose(it) + remove(it) + } _outputs = null } } @@ -186,10 +167,12 @@ class EditorCellView( .filter { it is NotebookCellInlayController.InputFactory } } - private fun failSafeCompute(factory: NotebookCellInlayController.Factory, - editor: Editor, - controllers: Collection, - intervalIterator: ListIterator): NotebookCellInlayController? { + private fun failSafeCompute( + factory: NotebookCellInlayController.Factory, + editor: Editor, + controllers: Collection, + intervalIterator: ListIterator, + ): NotebookCellInlayController? { try { return factory.compute(editor as EditorImpl, controllers, intervalIterator) } @@ -199,11 +182,6 @@ class EditorCellView( } } - fun updatePositions() { - input.updatePositions() - outputs?.updatePositions() - } - fun onViewportChanges() { input.onViewportChange() outputs?.onViewportChange() @@ -362,6 +340,24 @@ class EditorCellView( } } + override fun doInvalidate() { + cellInlayManager.invalidateCells() + } + + override fun calculateBounds(): Rectangle { + val inputBounds = input.calculateBounds() + val currentOutputs = outputs + return Rectangle( + 0, + inputBounds.y, + editor.contentSize.width, + currentOutputs?.calculateBounds() + ?.takeIf { !it.isEmpty } + ?.let { it.height + it.y - inputBounds.y } + ?: inputBounds.height + ) + } + inner class NotebookGutterLineMarkerRenderer(private val interval: NotebookCellLines.Interval) : NotebookLineMarkerRenderer() { override fun paint(editor: Editor, g: Graphics, r: Rectangle) { editor as EditorImpl @@ -381,10 +377,12 @@ class EditorCellView( } } - private fun paintBackground(editor: EditorImpl, - g: Graphics, - r: Rectangle, - interval: NotebookCellLines.Interval) { + private fun paintBackground( + editor: EditorImpl, + g: Graphics, + r: Rectangle, + interval: NotebookCellLines.Interval, + ) { for (controller: NotebookCellInlayController in controllers) { controller.paintGutter(editor, g, r, interval) } diff --git a/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/EditorCellViewComponent.kt b/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/EditorCellViewComponent.kt index 837c762f0315..f3413491cdf6 100644 --- a/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/EditorCellViewComponent.kt +++ b/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/EditorCellViewComponent.kt @@ -1,16 +1,64 @@ package org.jetbrains.plugins.notebooks.visualization.ui -import com.intellij.openapi.actionSystem.AnAction -import java.awt.Dimension -import java.awt.Point +import java.awt.Rectangle -interface EditorCellViewComponent { - val size: Dimension - val location: Point +abstract class EditorCellViewComponent { - fun updateGutterIcons(gutterAction: AnAction?) - fun dispose() - fun onViewportChange() - fun addViewComponentListener(listener: EditorCellViewComponentListener) - fun updatePositions() + var bounds: Rectangle = Rectangle(0, 0, 0, 0) + + private var parent: EditorCellViewComponent? = null + + private val children = mutableListOf() + + private var valid = false + + fun add(child: EditorCellViewComponent) { + children.add(child) + child.parent = this + } + + fun remove(child: EditorCellViewComponent) { + children.remove(child) + child.parent = null + } + + fun dispose() { + children.forEach { it.dispose() } + doDispose() + } + + open fun doDispose() {} + + fun onViewportChange() { + children.forEach { it.onViewportChange() } + doViewportChange() + } + + open fun doViewportChange() {} + + abstract fun calculateBounds(): Rectangle + + fun validate() { + if (!valid) { + doLayout() + children.forEach { it.validate() } + valid = true + } + } + + open fun doLayout() { + children.forEach { + it.bounds = it.calculateBounds() + } + } + + fun invalidate() { + if (valid) { + doInvalidate() + valid = false + parent?.invalidate() + } + } + + open fun doInvalidate() {} } \ No newline at end of file diff --git a/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/EditorCellViewComponentListener.kt b/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/EditorCellViewComponentListener.kt deleted file mode 100644 index f7c89f0c031f..000000000000 --- a/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/EditorCellViewComponentListener.kt +++ /dev/null @@ -1,11 +0,0 @@ -package org.jetbrains.plugins.notebooks.visualization.ui - -import java.awt.Dimension -import java.awt.Point -import java.util.EventListener - -interface EditorCellViewComponentListener : EventListener { - - fun componentBoundaryChanged(location: Point, size: Dimension) {} - -} \ No newline at end of file diff --git a/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/HasGutterIcon.kt b/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/HasGutterIcon.kt new file mode 100644 index 000000000000..8bcc28cc45e2 --- /dev/null +++ b/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/HasGutterIcon.kt @@ -0,0 +1,8 @@ +package org.jetbrains.plugins.notebooks.visualization.ui + +import com.intellij.openapi.actionSystem.AnAction + +interface HasGutterIcon { + fun updateGutterIcons(gutterAction: AnAction?) + +} \ No newline at end of file diff --git a/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/TextEditorCellViewComponent.kt b/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/TextEditorCellViewComponent.kt index 179c3f92222f..67438b60df83 100644 --- a/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/TextEditorCellViewComponent.kt +++ b/notebooks/visualization/src/org/jetbrains/plugins/notebooks/visualization/ui/TextEditorCellViewComponent.kt @@ -6,39 +6,20 @@ import com.intellij.openapi.editor.markup.HighlighterLayer import com.intellij.openapi.editor.markup.HighlighterTargetArea import com.intellij.openapi.editor.markup.RangeHighlighter import com.intellij.openapi.editor.markup.TextAttributes -import com.intellij.util.EventDispatcher import org.jetbrains.plugins.notebooks.visualization.NotebookCellLines import java.awt.Dimension -import java.awt.Point +import java.awt.Rectangle class TextEditorCellViewComponent( private val editor: EditorEx, - private val cell: EditorCell -) : EditorCellViewComponent { + private val cell: EditorCell, +) : EditorCellViewComponent(), HasGutterIcon { private var highlighters: List? = null private val interval: NotebookCellLines.Interval get() = cell.intervalPointer.get() ?: error("Invalid interval") - private val cellEventListeners = EventDispatcher.create(EditorCellViewComponentListener::class.java) - override val location: Point - get() { - val startOffset = editor.document.getLineStartOffset(interval.lines.first) - return editor.offsetToXY(startOffset) - } - - override val size: Dimension - get() { - val interval = interval - val startOffset = editor.document.getLineStartOffset(interval.lines.first) - val endOffset = editor.document.getLineEndOffset(interval.lines.last) - val location = editor.offsetToXY(startOffset) - val height = editor.offsetToXY(endOffset).y + editor.lineHeight - location.y - val width = editor.offsetToXY(endOffset).x - location.x - return Dimension(width, height) - } - override fun updateGutterIcons(gutterAction: AnAction?) { disposeExistingHighlighter() val action = gutterAction @@ -59,13 +40,10 @@ class TextEditorCellViewComponent( } } - override fun dispose() { + override fun doDispose() { disposeExistingHighlighter() } - override fun onViewportChange() { - } - private fun disposeExistingHighlighter() { if (highlighters != null) { highlighters?.forEach { @@ -75,11 +53,15 @@ class TextEditorCellViewComponent( } } - override fun addViewComponentListener(listener: EditorCellViewComponentListener) { - cellEventListeners.addListener(listener) + override fun calculateBounds(): Rectangle { + val startOffset = editor.document.getLineStartOffset(interval.lines.first) + val location = editor.offsetToXY(startOffset) + val interval = interval + val endOffset = editor.document.getLineEndOffset(interval.lines.last) + val height = editor.offsetToXY(endOffset).y + editor.lineHeight - location.y + val width = editor.offsetToXY(endOffset).x - location.x + val dimension = Dimension(width, height) + return Rectangle(location, dimension) } - override fun updatePositions() { - cellEventListeners.multicaster.componentBoundaryChanged(location, size) - } } \ No newline at end of file