[PyCharm] PY-66520 Jupyter (refactor): Nested scrolling rewritten, layers and ancestor listeners removed, timeout tweaked.

GitOrigin-RevId: 2596450201eaaa08855d3041f12e5dc12ad553b1
This commit is contained in:
Nikita Pavlenko
2024-09-17 13:03:53 +02:00
committed by intellij-monorepo-bot
parent dfdce84cf9
commit 3106110b50
4 changed files with 238 additions and 247 deletions

View File

@@ -1,6 +1,5 @@
package com.intellij.notebooks.visualization.ui
import com.intellij.execution.console.LanguageConsoleImpl
import com.intellij.notebooks.ui.editor.actions.command.mode.NotebookEditorMode
import com.intellij.notebooks.ui.editor.actions.command.mode.setMode
import com.intellij.notebooks.visualization.*
@@ -14,38 +13,33 @@ import com.intellij.openapi.editor.Caret
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.event.*
import com.intellij.openapi.editor.ex.util.EditorScrollingPositionKeeper
import com.intellij.openapi.editor.impl.EditorComponentImpl
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.fileEditor.impl.text.TextEditorComponent
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.use
import com.intellij.ui.AncestorListenerAdapter
import java.awt.*
import java.awt.BorderLayout
import java.awt.Component
import java.awt.GraphicsEnvironment
import java.awt.Point
import java.awt.event.InputEvent
import java.awt.event.MouseEvent
import java.awt.event.MouseWheelEvent
import java.util.concurrent.atomic.AtomicBoolean
import javax.swing.JComponent
import javax.swing.JLayer
import javax.swing.JPanel
import javax.swing.SwingUtilities
import javax.swing.event.AncestorEvent
import javax.swing.plaf.LayerUI
import kotlin.math.max
import kotlin.math.min
class DecoratedEditor private constructor(private val editorImpl: EditorImpl, private val manager: NotebookCellInlayManager) : NotebookEditor {
class DecoratedEditor private constructor(
private val editorImpl: EditorImpl,
private val manager: NotebookCellInlayManager,
) : NotebookEditor {
/** Used to hold current cell under mouse, to update the folding state and "run" button state. */
private var mouseOverCell: EditorCellView? = null
private val selectionModel = EditorCellSelectionModel(manager)
private var selectionUpdateScheduled: AtomicBoolean = AtomicBoolean(false)
/**
* Correct parent for the editor component - our special scroll-supporting layer.
* We cannot wrap editorComponent at creation, so we are changing its parent du
*/
private var editorComponentParent: JLayer<JComponent>? = null
private var selectionUpdateScheduled = AtomicBoolean(false)
init {
if (!GraphicsEnvironment.isHeadless()) {
@@ -79,17 +73,9 @@ class DecoratedEditor private constructor(private val editorImpl: EditorImpl, pr
}, editorImpl.disposable)
editorImpl.caretModel.addCaretListener(object : CaretListener {
override fun caretAdded(event: CaretEvent) {
scheduleSelectionUpdate()
}
override fun caretPositionChanged(event: CaretEvent) {
scheduleSelectionUpdate()
}
override fun caretRemoved(event: CaretEvent) {
scheduleSelectionUpdate()
}
override fun caretAdded(event: CaretEvent) = scheduleSelectionUpdate()
override fun caretPositionChanged(event: CaretEvent) = scheduleSelectionUpdate()
override fun caretRemoved(event: CaretEvent) = scheduleSelectionUpdate()
})
updateSelectionByCarets()
@@ -98,46 +84,36 @@ class DecoratedEditor private constructor(private val editorImpl: EditorImpl, pr
}
private fun wrapEditorComponent(editor: EditorImpl) {
val parent = editor.component.parent
if (parent == null || parent == editorComponentParent) {
val nestedScrollingSupport = NestedScrollingSupportImpl()
editorImpl.component.addAncestorListener(object : AncestorListenerAdapter() {
override fun ancestorAdded(event: AncestorEvent?) {
wrapEditorComponent(editorImpl)
NotebookAWTMouseDispatcher(editor.scrollPane).apply {
eventDispatcher.addListener { event ->
if (event is MouseEvent) {
getEditorPoint(event)?.let { (_, point) ->
updateMouseOverCell(point)
}
}
})
}
return
eventDispatcher.addListener { event ->
if (event is MouseWheelEvent) {
nestedScrollingSupport.processMouseWheelEvent(event)
}
else if (event is MouseEvent) {
if (event.id == MouseEvent.MOUSE_CLICKED || event.id == MouseEvent.MOUSE_RELEASED || event.id == MouseEvent.MOUSE_PRESSED) {
nestedScrollingSupport.processMouseEvent(event, editor.scrollPane)
}
else if (event.id == MouseEvent.MOUSE_MOVED) {
nestedScrollingSupport.processMouseMotionEvent(event)
}
}
}
Disposer.register(editor.disposable, this)
}
if(parent is LanguageConsoleImpl.ConsoleEditorsPanel) return
val view = editorImpl.scrollPane.viewport.view
if (view is EditorComponentImpl) {
editorImpl.scrollPane.viewport.view = EditorComponentWrapper(editorImpl, view)
}
editorComponentParent = createCellUnderMouseSupportLayer(editorImpl.component)
val secondLayer = NestedScrollingSupport.addNestedScrollingSupport(editorComponentParent!!)
parent.remove(editor.component)
val newComponent = secondLayer
if (parent is TextEditorComponent) {
parent.__add(newComponent, GridBagConstraints().also {
it.gridx = 0
it.gridy = 0
it.weightx = 1.0
it.weighty = 1.0
it.fill = GridBagConstraints.BOTH
})
}
else if (parent is LanguageConsoleImpl.ConsoleEditorsPanel) {
parent.add(newComponent)
}
else {
parent.add(newComponent, BorderLayout.CENTER)
}
editor.scrollPane.viewport.view = EditorComponentWrapper(editor, editor.contentComponent)
}
/** The main thing while we need it - to perform updating of underlying components within keepScrollingPositionWhile. */
@@ -193,27 +169,6 @@ class DecoratedEditor private constructor(private val editorImpl: EditorImpl, pr
}
}
private fun createCellUnderMouseSupportLayer(view: JComponent) = JLayer(view, object : LayerUI<JComponent>() {
override fun installUI(c: JComponent) {
super.installUI(c)
(c as JLayer<*>).layerEventMask = AWTEvent.MOUSE_MOTION_EVENT_MASK
}
override fun uninstallUI(c: JComponent) {
super.uninstallUI(c)
(c as JLayer<*>).layerEventMask = 0
}
override fun eventDispatched(e: AWTEvent, l: JLayer<out JComponent?>?) {
if (e is MouseEvent) {
getEditorPoint(e)?.let { (_, point) ->
updateMouseOverCell(point)
}
}
}
})
private fun getEditorPoint(e: MouseEvent): Pair<Component, Point>? {
val component = if (SwingUtilities.isDescendingFrom(e.component, editorImpl.contentComponent)) {
editorImpl.contentComponent

View File

@@ -1,203 +1,194 @@
package com.intellij.notebooks.visualization.ui
import com.intellij.openapi.util.registry.Registry
import com.intellij.ui.ComponentUtil
import java.awt.AWTEvent
import java.awt.Component
import java.awt.event.MouseEvent
import java.awt.event.MouseWheelEvent
import javax.swing.JComponent
import javax.swing.JLayer
import javax.swing.JScrollPane
import javax.swing.SwingUtilities
import javax.swing.plaf.LayerUI
import kotlin.reflect.KClass
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds
import kotlin.time.DurationUnit
/**
* Decorates component to handle nested scrolling areas gracefully.
* As it described in [Mozilla documentation](https://wiki.mozilla.org/Gecko:Mouse_Wheel_Scrolling#Mouse_wheel_transaction)
* Processes Mouse (wheel, motion, click) to handle nested scrolling areas gracefully. Used together with [NotebookAWTMouseDispatcher].
* Nested scrolling idea described in [Mozilla documentation](https://wiki.mozilla.org/Gecko:Mouse_Wheel_Scrolling#Mouse_wheel_transaction)
*/
object NestedScrollingSupport {
class NestedScrollingSupportImpl {
private val asyncComponents = mutableSetOf<KClass<*>>()
private var _currentMouseWheelOwner: Component? = null
private var currentMouseWheelOwner: Component?
get() {
return resetOwnerIfTimeoutExceeded()
}
set(value) {
_currentMouseWheelOwner = value
}
fun registerAsyncComponent(type: KClass<*>) {
asyncComponents.add(type)
private var timestamp = 0L
private var dispatchingEvent: MouseEvent? = null
private fun isNewEventCreated(e: MouseWheelEvent) = dispatchingEvent != e
private fun isDispatchingInProgress() = dispatchingEvent != null
fun processMouseWheelEvent(e: MouseWheelEvent) {
val component = e.component
if (isDispatchingInProgress()) {
if (!isNewEventCreated(e)) {
return
}
else if (_currentMouseWheelOwner != null) {
// Prevents [JBScrollPane] from propagating wheel events to the parent component if there is an active scroll
e.consume()
return
}
}
resetOwnerIfTimeoutExceeded()
val owner = resetOwnerIfEventIsOutside(e)
if (owner != null) {
if (component != owner) {
redispatchEvent(SwingUtilities.convertMouseEvent(component, e, owner))
e.consume()
}
else {
dispatchEvent(e)
}
}
}
fun addNestedScrollingSupport(view: JComponent): JLayer<JComponent> {
return JLayer(view, object : LayerUI<JComponent>() {
fun processMouseEvent(e: MouseEvent, scrollPane: JScrollPane) {
if (e.id == MouseEvent.MOUSE_CLICKED || e.id == MouseEvent.MOUSE_RELEASED || e.id == MouseEvent.MOUSE_PRESSED) {
updateOwner(scrollPane)
}
}
private var _currentMouseWheelOwner: Component? = null
private var currentMouseWheelOwner: Component?
get() {
return resetOwnerIfTimeoutExceeded()
}
set(value) {
_currentMouseWheelOwner = value
}
fun processMouseMotionEvent(e: MouseEvent) {
val owner = currentMouseWheelOwner
if (owner != null && isTimeoutExceeded(100.milliseconds) && !isEventInsideOwner(owner, e)) {
resetOwner()
}
}
private var timestamp = 0L
private fun dispatchEvent(event: MouseEvent) {
val owner = event.component
if (isAsync(owner)) return
private var dispatchingEvent: MouseEvent? = null
override fun installUI(c: JComponent) {
super.installUI(c)
(c as JLayer<*>).layerEventMask = AWTEvent.MOUSE_WHEEL_EVENT_MASK or AWTEvent.MOUSE_EVENT_MASK or AWTEvent.MOUSE_MOTION_EVENT_MASK
if (owner is JLayer<*>) {
val aa = owner.parent
if (aa is JLayer<*>) {
dispatchEventSync(event, aa.parent)
}
override fun uninstallUI(c: JComponent) {
super.uninstallUI(c)
(c as JLayer<*>).layerEventMask = 0
else {
dispatchEventSync(event, aa)
}
}
else {
dispatchEventSync(event, owner)
}
}
override fun processMouseWheelEvent(e: MouseWheelEvent, l: JLayer<out JComponent>) {
val component = e.component
if (isDispatchingInProgress()) {
if (!isNewEventCreated(e)) {
return
}
else if (_currentMouseWheelOwner != null) {
// Prevents [JBScrollPane] from propagating wheel events to the parent component if there is an active scroll
e.consume()
return
}
}
resetOwnerIfTimeoutExceeded()
val owner = resetOwnerIfEventIsOutside(e)
if (owner != null) {
if (component != owner) {
redispatchEvent(SwingUtilities.convertMouseEvent(component, e, owner))
e.consume()
}
else {
dispatchEvent(e)
}
}
private fun dispatchEventSync(event: MouseEvent, owner: Component) {
val oldDispatchingEvent = dispatchingEvent
dispatchingEvent = event
try {
owner.dispatchEvent(event)
if (event.isConsumed && _currentMouseWheelOwner == null) {
updateOwner(owner)
}
private fun isNewEventCreated(e: MouseWheelEvent) = dispatchingEvent != e
private fun isDispatchingInProgress() = dispatchingEvent != null
private fun isAsync(owner: Component): Boolean {
return asyncComponents.contains(owner::class)
else {
updateOwner(_currentMouseWheelOwner)
}
}
finally {
dispatchingEvent = oldDispatchingEvent
}
}
private fun dispatchEvent(event: MouseEvent) {
val owner = event.component
if (!isAsync(owner)) {
dispatchEventSync(event, owner)
}
}
private fun redispatchEvent(event: MouseEvent) {
val oldDispatchingEvent = dispatchingEvent
dispatchingEvent = null
try {
val owner = event.component
owner.dispatchEvent(event)
}
finally {
dispatchingEvent = oldDispatchingEvent
}
}
private fun dispatchEventSync(event: MouseEvent, owner: Component): Boolean {
val oldDispatchingEvent = dispatchingEvent
dispatchingEvent = event
try {
owner.dispatchEvent(event)
if (event.isConsumed && _currentMouseWheelOwner == null) {
updateOwner(owner)
}
else {
updateOwner(_currentMouseWheelOwner)
}
return event.isConsumed
}
finally {
dispatchingEvent = oldDispatchingEvent
}
}
private fun resetOwnerIfTimeoutExceeded(): Component? {
val currentOwner = _currentMouseWheelOwner
if (currentOwner == null) {
return null
}
val scrollOwnerTimeout = Registry.intValue("jupyter.editor.scroll.mousewheel.timeout", 750).milliseconds
return if (isTimeoutExceeded(scrollOwnerTimeout)) {
resetOwner()
null
}
else {
currentOwner
}
}
private fun redispatchEvent(event: MouseEvent): Boolean {
val oldDispatchingEvent = dispatchingEvent
dispatchingEvent = null
try {
val owner = event.component
owner.dispatchEvent(event)
return event.isConsumed
}
finally {
dispatchingEvent = oldDispatchingEvent
}
}
private fun resetOwnerIfEventIsOutside(e: MouseWheelEvent): Component? {
val currentOwner = _currentMouseWheelOwner
return if (currentOwner != null && isEventInsideOwner(currentOwner, e)) {
currentOwner
}
else {
resetOwner()
e.component
}
}
private fun resetOwnerIfTimeoutExceeded(): Component? {
val currentOwner = _currentMouseWheelOwner
if (currentOwner == null) {
return null
}
val scrollOwnerTimeout = Registry.intValue("jupyter.editor.scroll.mousewheel.timeout", 1000).milliseconds
return if (isTimeoutExceeded(scrollOwnerTimeout)) {
resetOwner()
null
}
else {
currentOwner
}
}
private fun isEventInsideOwner(owner: Component, e: MouseEvent): Boolean {
val component = e.component
return if (component == null) {
false
}
else {
val p = SwingUtilities.convertPoint(component, e.point, owner)
return owner.contains(p)
}
}
private fun resetOwnerIfEventIsOutside(e: MouseWheelEvent): Component? {
val currentOwner = _currentMouseWheelOwner
return if (currentOwner != null && isEventInsideOwner(currentOwner, e)) {
currentOwner
}
else {
resetOwner()
e.component
}
}
private fun isTimeoutExceeded(timeout: Duration): Boolean {
return timestamp + timeout.toInt(DurationUnit.NANOSECONDS) < System.nanoTime()
}
private fun isEventInsideOwner(owner: Component, e: MouseEvent): Boolean {
val component = e.component
return if (component == null) {
false
}
else {
val p = SwingUtilities.convertPoint(component, e.point, owner)
return owner.contains(p)
}
}
private fun updateOwner(component: Component?) {
if (component != null) {
replaceOwner(component)
}
else {
resetOwner()
}
}
private fun isTimeoutExceeded(timeout: Duration): Boolean {
return timestamp + timeout.toInt(DurationUnit.NANOSECONDS) < System.nanoTime()
}
private fun replaceOwner(component: Component) {
_currentMouseWheelOwner = component
timestamp = System.nanoTime()
}
private fun updateOwner(component: Component?) {
if (component != null) {
replaceOwner(component)
}
else {
resetOwner()
}
}
private fun resetOwner() {
timestamp = 0
_currentMouseWheelOwner = null
}
private fun replaceOwner(component: Component) {
currentMouseWheelOwner = component
timestamp = System.nanoTime()
}
private fun isAsync(owner: Component): Boolean {
return asyncComponents.contains(owner::class)
}
private fun resetOwner() {
timestamp = 0
currentMouseWheelOwner = null
}
companion object {
internal val asyncComponents = mutableSetOf<KClass<*>>()
override fun processMouseEvent(e: MouseEvent, l: JLayer<out JComponent>) {
if (e.id == MouseEvent.MOUSE_CLICKED || e.id == MouseEvent.MOUSE_RELEASED || e.id == MouseEvent.MOUSE_PRESSED) {
val scrollPane = ComponentUtil.getParentOfType(JScrollPane::class.java, l.findComponentAt(e.point))
updateOwner(scrollPane)
}
}
override fun processMouseMotionEvent(e: MouseEvent, l: JLayer<out JComponent>) {
val owner = currentMouseWheelOwner
if (owner != null && isTimeoutExceeded(100.milliseconds) && !isEventInsideOwner(owner, e)) {
resetOwner()
}
}
})
fun registerAsyncComponent(type: KClass<*>) {
asyncComponents.add(type)
}
}
}

View File

@@ -0,0 +1,45 @@
package com.intellij.notebooks.visualization.ui
import com.intellij.openapi.Disposable
import com.intellij.util.EventDispatcher
import java.awt.AWTEvent
import java.awt.Component
import java.awt.Toolkit
import java.awt.event.AWTEventListener
import java.awt.event.ComponentEvent
import java.awt.event.InputEvent
/**
* Dispatches mouse events (click, move, wheel) which have [target] component or any of its children.
*
* @property target the root Component for which the events will be collected.
*/
class NotebookAWTMouseDispatcher(private val target: Component) : Disposable, AWTEventListener {
val eventDispatcher = EventDispatcher.create(AWTEventListener::class.java)
init {
Toolkit.getDefaultToolkit().addAWTEventListener(this,
AWTEvent.MOUSE_WHEEL_EVENT_MASK or
AWTEvent.MOUSE_EVENT_MASK or
AWTEvent.MOUSE_MOTION_EVENT_MASK)
}
override fun dispose() {
Toolkit.getDefaultToolkit().removeAWTEventListener(this)
}
override fun eventDispatched(event: AWTEvent) {
if (event !is ComponentEvent) return
var component: Component? = event.component
while (component != null) {
if (component == target) {
if (event is InputEvent && !event.isConsumed) {
eventDispatcher.getMulticaster().eventDispatched(event)
}
}
component = component.getParent()
}
}
}

View File

@@ -334,7 +334,7 @@ public final class EditorEmbeddedComponentManager {
}
});
renderer.addMouseWheelListener(myEditor.getContentComponent()::dispatchEvent);
// renderer.addMouseWheelListener(myEditor.getContentComponent()::dispatchEvent);
renderer.setInlay(inlay);
myEditor.getContentComponent().add(renderer);