diff --git a/notebooks/visualization/src/com/intellij/notebooks/visualization/ui/DecoratedEditor.kt b/notebooks/visualization/src/com/intellij/notebooks/visualization/ui/DecoratedEditor.kt index ba85261f78ae..c01c2b4ccda1 100644 --- a/notebooks/visualization/src/com/intellij/notebooks/visualization/ui/DecoratedEditor.kt +++ b/notebooks/visualization/src/com/intellij/notebooks/visualization/ui/DecoratedEditor.kt @@ -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? = 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() { - - 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?) { - if (e is MouseEvent) { - getEditorPoint(e)?.let { (_, point) -> - updateMouseOverCell(point) - } - } - } - }) - private fun getEditorPoint(e: MouseEvent): Pair? { val component = if (SwingUtilities.isDescendingFrom(e.component, editorImpl.contentComponent)) { editorImpl.contentComponent diff --git a/notebooks/visualization/src/com/intellij/notebooks/visualization/ui/NestedScrollingSupport.kt b/notebooks/visualization/src/com/intellij/notebooks/visualization/ui/NestedScrollingSupport.kt index 493d32b245f4..b7507c216e90 100644 --- a/notebooks/visualization/src/com/intellij/notebooks/visualization/ui/NestedScrollingSupport.kt +++ b/notebooks/visualization/src/com/intellij/notebooks/visualization/ui/NestedScrollingSupport.kt @@ -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>() + 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 { - return JLayer(view, object : LayerUI() { + 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) { - 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>() - override fun processMouseEvent(e: MouseEvent, l: JLayer) { - 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) { - val owner = currentMouseWheelOwner - if (owner != null && isTimeoutExceeded(100.milliseconds) && !isEventInsideOwner(owner, e)) { - resetOwner() - } - } - }) + fun registerAsyncComponent(type: KClass<*>) { + asyncComponents.add(type) + } } } \ No newline at end of file diff --git a/notebooks/visualization/src/com/intellij/notebooks/visualization/ui/NotebookAWTMouseDispatcher.kt b/notebooks/visualization/src/com/intellij/notebooks/visualization/ui/NotebookAWTMouseDispatcher.kt new file mode 100644 index 000000000000..1fa0e19cfbfb --- /dev/null +++ b/notebooks/visualization/src/com/intellij/notebooks/visualization/ui/NotebookAWTMouseDispatcher.kt @@ -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() + } + } +} \ No newline at end of file diff --git a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorEmbeddedComponentManager.java b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorEmbeddedComponentManager.java index 421c84039cf4..6c32fae0b0bd 100644 --- a/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorEmbeddedComponentManager.java +++ b/platform/platform-impl/src/com/intellij/openapi/editor/impl/EditorEmbeddedComponentManager.java @@ -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);