[jupyter] PY-66455 heavyweight jupyter toolbar

GitOrigin-RevId: 11b9129165e643c33d3c8ff7be6390d49c3e44a5
This commit is contained in:
Bogdan Kirilenko
2024-04-02 16:18:45 +00:00
committed by intellij-monorepo-bot
parent fffb2b1459
commit 5000e32a42
6 changed files with 236 additions and 212 deletions
@@ -1,8 +1,10 @@
package org.jetbrains.plugins.notebooks.ui.jupyterToolbar
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.actionSystem.ActionPlaces
import com.intellij.openapi.actionSystem.impl.ActionToolbarImpl
import com.intellij.openapi.actionSystem.toolbarLayout.ToolbarLayoutStrategy
import com.intellij.openapi.editor.Editor
import com.intellij.ui.JBColor
import com.intellij.ui.NewUiValue
@@ -14,23 +16,27 @@ import java.awt.Cursor
import java.awt.Graphics
import java.awt.Graphics2D
import javax.swing.BorderFactory
import javax.swing.JComponent
import javax.swing.SwingUtilities
/**
* @See com.intellij.bigdatatools.visualization.inlays.components.FadingToolbar
* PY-66455
*/
class JupyterToolbar(actionGroup: ActionGroup) : ActionToolbarImpl(ActionPlaces.EDITOR_INLAY, actionGroup, true) {
class JupyterToolbar(actionGroup: ActionGroup, target: JComponent) :
ActionToolbarImpl(ActionPlaces.EDITOR_INLAY, actionGroup, true)
{
init {
val borderColor = when (NewUiValue.isEnabled()) {
true ->JBColor.LIGHT_GRAY
else -> JBColor.DARK_GRAY
true -> JBColor.LIGHT_GRAY
else -> JBColor.GRAY
}
border = BorderFactory.createCompoundBorder(RoundedLineBorder(borderColor, TOOLBAR_ARC_SIZE, TOOLBAR_BORDER_THICKNESS),
BorderFactory.createEmptyBorder(OUTER_PADDING, OUTER_PADDING, OUTER_PADDING, OUTER_PADDING))
isOpaque = false
cursor = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR)
targetComponent = target
setSkipWindowAdjustments(false)
}
@@ -51,6 +57,7 @@ class JupyterToolbar(actionGroup: ActionGroup) : ActionToolbarImpl(ActionPlaces.
if (!StartupUiUtil.isDarkTheme) {
background = JBColor.WHITE
}
layoutStrategy = ToolbarLayoutStrategy.NOWRAP_STRATEGY
}
fun getRespectiveLineNumberInEditor(editor: Editor): Int? {
@@ -1,158 +0,0 @@
package org.jetbrains.plugins.notebooks.ui.jupyterToolbar
import com.intellij.ide.ui.customization.CustomActionsSchema
import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.ui.scale.JBUIScale
import java.awt.Dimension
import java.awt.MouseInfo
import java.awt.Point
import java.awt.Rectangle
import java.awt.event.*
import javax.swing.JPanel
import javax.swing.SwingUtilities
import javax.swing.Timer
class JupyterToolbarManager(
private val editor: EditorImpl,
private val panel: JPanel,
private val actionGroupId: String
) { // See PY-66455
private var toolbar: JupyterToolbar? = null
private var hideToolbarTimer = Timer(TOOLBAR_HIDE_DELAY) { conditionallyHideToolBar() }
init {
initPanelMouseListeners()
addPanelComponentListener()
addEditorKeyListener()
addEditorComponentListener()
}
private fun initPanelMouseListeners() {
val mouseAdapter = object : MouseAdapter() {
override fun mouseEntered(e: MouseEvent) {
hideToolbarTimer.stop()
showToolbar()
}
override fun mouseExited(e: MouseEvent) {
hideToolbarTimer.restart()
}
}
panel.addMouseListener(mouseAdapter)
}
private fun addPanelComponentListener() {
val componentListener = object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent?) {
super.componentResized(e)
toolbar?.let {
it.bounds = calculateToolbarBounds(editor, panel, it.preferredSize)
panel.revalidate()
panel.repaint()
}
}
override fun componentMoved(e: ComponentEvent?) {
super.componentMoved(e)
toolbar?.let {
it.bounds = calculateToolbarBounds(editor, panel, it.preferredSize)
panel.revalidate()
panel.repaint()
}
}
}
panel.addComponentListener(componentListener)
}
private fun addEditorKeyListener() {
val keyAdapter = object : KeyAdapter() {
override fun keyTyped(e: KeyEvent) = hideToolBar()
override fun keyPressed(e: KeyEvent) = hideToolBar()
}
editor.contentComponent.addKeyListener(keyAdapter)
}
private fun addEditorComponentListener() {
editor.contentComponent.addComponentListener(object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent?) = hideToolBar()
})
}
/**
* Checks if the mouse is over the toolbar before hiding it.
* This is necessary because mouseExited events on the panel can trigger even when the mouse
* is over the toolbar due to its position in the Z-stack.
* If the mouse is indeed over the toolbar, hiding is deferred to prevent an unintended disappearance,
* acting as an additional safeguard for better user experience.
*/
private fun conditionallyHideToolBar() {
toolbar?.let { tb ->
val mousePos = MouseInfo.getPointerInfo().location
SwingUtilities.convertPointFromScreen(mousePos, editor.contentComponent)
if (tb.bounds.contains(mousePos)) {
hideToolbarTimer.restart()
} else {
hideToolBar() // mouse is not over the toolbar - we may hide it
}
}
}
fun hideToolBar() {
toolbar?.let {
editor.contentComponent.remove(it)
editor.contentComponent.revalidate()
editor.contentComponent.repaint()
toolbar = null
JupyterToolbarVisibilityManager.notifyToolbarHidden(this)
}
}
private fun showToolbar() {
if (toolbar == null) {
val actionGroup = createActionGroup() ?: return
toolbar = JupyterToolbar(actionGroup).apply {
targetComponent = editor.contentComponent
}
}
JupyterToolbarVisibilityManager.requestToolbarDisplay(this)
editor.contentComponent.add(toolbar, 0)
positionToolbar()
}
private fun positionToolbar() {
toolbar?.let { tb ->
val bounds = calculateToolbarBounds(editor, panel, tb.preferredSize)
tb.bounds = bounds
}
}
private fun createActionGroup(): ActionGroup? {
return CustomActionsSchema.getInstance().getCorrectedAction(actionGroupId) as? ActionGroup
}
companion object {
private val DEFAULT_Y_OFFSET = JBUIScale.scale(-10)
private const val TOOLBAR_HIDE_DELAY = 800
fun calculateToolbarBounds(
editor: EditorImpl,
panel: JPanel,
toolbar: Dimension,
extraYOffset: Int = 0
) : Rectangle {
val xOffset = (panel.width - toolbar.width) / 2
val yOffset = DEFAULT_Y_OFFSET + extraYOffset
val editorComponent = editor.contentComponent
val panelLocationInEditor = SwingUtilities.convertPoint(panel, Point(0, 0), editorComponent)
val xCoordinate = panelLocationInEditor.x + xOffset
val yCoordinate = panelLocationInEditor.y + yOffset
return Rectangle(xCoordinate, yCoordinate, toolbar.width, toolbar.height)
}
}
}
@@ -0,0 +1,53 @@
package org.jetbrains.plugins.notebooks.ui.jupyterToolbar
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.project.Project
import java.awt.event.ComponentEvent
import java.awt.event.ComponentListener
import java.awt.event.MouseAdapter
import java.awt.event.MouseEvent
import javax.swing.JPanel
/**
* Adds listeners for "above cell" panels to trigger toolbar-related functions in the [JupyterToolbarService].
*/
class JupyterToolbarPanelListeners( // PY-66455
private val panel: JPanel,
project: Project,
private val editor: EditorImpl
) : MouseAdapter(), ComponentListener {
private val toolbarService = JupyterToolbarService.getInstance(project)
init {
panel.addMouseListener(this)
panel.addComponentListener(this)
}
override fun mouseEntered(e: MouseEvent?) {
toolbarService.requestToolbarDisplay(panel, editor)
}
override fun mouseExited(e: MouseEvent?) {
toolbarService.requestToolbarHide()
}
override fun mouseClicked(e: MouseEvent?) {
toolbarService.hideToolbarUnconditionally()
}
override fun componentResized(e: ComponentEvent?) {
toolbarService.adjustToolbarPosition()
}
override fun componentMoved(e: ComponentEvent?) {
toolbarService.adjustToolbarPosition()
}
override fun componentShown(e: ComponentEvent?) {
toolbarService.hideToolbarUnconditionally()
}
override fun componentHidden(e: ComponentEvent?) {
toolbarService.hideToolbarUnconditionally()
}
}
@@ -0,0 +1,148 @@
package org.jetbrains.plugins.notebooks.ui.jupyterToolbar
import com.intellij.ide.ui.customization.CustomActionsSchema
import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.components.Service
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.popup.JBPopup
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.openapi.util.Disposer
import com.intellij.ui.awt.RelativePoint
import org.jetbrains.annotations.ApiStatus
import org.jetbrains.plugins.notebooks.ui.visualization.DefaultNotebookEditorAppearanceSizes
import org.jetbrains.plugins.notebooks.ui.visualization.NotebookAboveCellDelimiterPanelNew
import java.awt.MouseInfo
import java.awt.Point
import javax.swing.JPanel
import javax.swing.SwingUtilities
import javax.swing.Timer
/**
* This class encapsulates a service that manages the display toggle action of a [JupyterToolbar] among multiple cells.
*
* The following rules apply:
* - The toolbar is always positioned beneath the last cell (not managed by this service)
* - Only one additional toolbar is allowed among cells (managed by this service)
*
* When a cursor hovers over a [NotebookAboveCellDelimiterPanelNew], a toolbar will appear.
* If a toolbar is already displayed above another panel, this older instance will be hidden.
*/
@ApiStatus.Internal
@Service(Service.Level.PROJECT)
class JupyterToolbarService { // PY-66455
private var currentPanel: JPanel? = null
private var currentToolbar: JupyterToolbar? = null
private var currentToolbarPopup: JBPopup? = null
private val hideToolbarTimer = Timer(TOOLBAR_HIDE_DELAY) { hideToolbarConditionally() }
private val actionGroup: ActionGroup? = createActionGroup()
fun requestToolbarDisplay(panel: JPanel, editor: EditorImpl) {
val shouldDisplayToolbar = currentPanel != panel || currentToolbarPopup == null || currentToolbarPopup?.isDisposed == true
if (shouldDisplayToolbar) {
hideToolbarUnconditionally()
currentPanel = panel
createAndShowToolbar(panel, editor)
}
}
fun requestToolbarHide() = hideToolbarTimer.restart()
private fun createAndShowToolbar(panel: JPanel, editor: EditorImpl) {
actionGroup ?: return
if (currentToolbar == null) {
currentToolbar = JupyterToolbar(actionGroup, editor.contentComponent)
}
if (currentToolbarPopup == null) {
currentToolbarPopup = JBPopupFactory.getInstance()
.createComponentPopupBuilder(currentToolbar!!.component, panel)
.setCancelOnClickOutside(true)
.setCancelKeyEnabled(true)
.setShowBorder(false)
.setShowShadow(false)
.createPopup().also {
val point = calculatePopupLocation() ?: return
it.show(RelativePoint(panel, point))
}
}
hideToolbarTimer.stop()
adjustToolbarPosition()
}
/**
* Checks if the mouse is over the toolbar before hiding it.
* This is necessary because mouseExited events on the panel can trigger even when the mouse
* is over the toolbar due to its position in the Z-stack.
* If the mouse is indeed over the toolbar, hiding is deferred to prevent an unintended disappearance,
* acting as an additional safeguard for better user experience.
*/
private fun hideToolbarConditionally() {
if (currentToolbar == null || currentToolbarPopup == null || currentToolbarPopup!!.isDisposed) return
try {
val mousePos = MouseInfo.getPointerInfo().location
val content = currentToolbarPopup?.content
if (content != null) {
SwingUtilities.convertPointFromScreen(mousePos, content)
if (currentToolbar?.bounds?.contains(mousePos) == true) {
hideToolbarTimer.restart()
return
} else {
hideToolbarUnconditionally()
}
}
} catch (e: IllegalStateException) { hideToolbarUnconditionally() }
}
fun hideToolbarUnconditionally() {
currentToolbar = null
currentPanel = null
currentToolbarPopup?.let { p ->
p.cancel()
if (!p.isDisposed) { Disposer.dispose(p) }
currentToolbarPopup = null
}
}
fun adjustToolbarPosition() {
val toolbarPopup = currentToolbarPopup ?: return
val panel = currentPanel ?: return
// I had to use this method to correctly evaluate the toolbar's width
currentToolbar?.updateActionsImmediately() ?: return
calculatePopupLocation()?.let {
val relPoint = RelativePoint(panel, it)
toolbarPopup.setLocation(relPoint.screenPoint)
} ?: run {
hideToolbarUnconditionally()
return
}
}
private fun calculatePopupLocation(): Point? {
val panelHeight = currentPanel?.height ?: return null
val panelWidth = currentPanel?.width ?: return null
val toolbarHeight = currentToolbar?.height ?: return null
val toolbarWidth = currentToolbar?.width ?: return null
val xOffset = (panelWidth - toolbarWidth) / 2
val yOffset = (panelHeight - (1.5 * DELIMITER_SIZE) - (toolbarHeight / 2)).toInt()
val result = Point(xOffset, yOffset)
return result
}
private fun createActionGroup(): ActionGroup? = CustomActionsSchema.getInstance().getCorrectedAction(ACTION_GROUP_ID) as? ActionGroup
companion object {
private const val ACTION_GROUP_ID = "Jupyter.AboveCellPanelNew"
private const val TOOLBAR_HIDE_DELAY = 600
private val DELIMITER_SIZE = DefaultNotebookEditorAppearanceSizes.CELL_BORDER_HEIGHT / 2
fun getInstance(project: Project): JupyterToolbarService = project.getService(JupyterToolbarService::class.java)
}
}
@@ -1,20 +0,0 @@
package org.jetbrains.plugins.notebooks.ui.jupyterToolbar
object JupyterToolbarVisibilityManager {
// simple singleton to keep only one active intercellular toolbar
// not sure whether a more complex observer solution is actually needed
private var currentActiveManager: JupyterToolbarManager? = null
fun requestToolbarDisplay(manager: JupyterToolbarManager) {
if (currentActiveManager != manager) {
currentActiveManager?.hideToolBar()
currentActiveManager = manager
}
}
fun notifyToolbarHidden(manager: JupyterToolbarManager) {
if (manager == currentActiveManager) {
currentActiveManager = null
}
}
}
@@ -4,14 +4,16 @@ import com.intellij.ide.ui.customization.CustomActionsSchema
import com.intellij.openapi.actionSystem.ActionGroup
import com.intellij.openapi.editor.EditorKind
import com.intellij.openapi.editor.impl.EditorImpl
import com.intellij.ui.scale.JBUIScale
import org.jetbrains.plugins.notebooks.ui.jupyterToolbar.JupyterToolbar
import org.jetbrains.plugins.notebooks.ui.jupyterToolbar.JupyterToolbarManager
import java.awt.Dimension
import java.awt.GridBagLayout
import java.awt.Point
import java.awt.Rectangle
import java.awt.event.ComponentAdapter
import java.awt.event.ComponentEvent
import javax.swing.JPanel
import javax.swing.SwingUtilities
class NotebookBelowLastCellPanelNew(val editor: EditorImpl) : JPanel(GridBagLayout()) {
private var toolbar: JupyterToolbar? = null
@@ -26,17 +28,9 @@ class NotebookBelowLastCellPanelNew(val editor: EditorImpl) : JPanel(GridBagLayo
fun initialize() {
// this toolbar is special - persistent and unique
val actionGroup = createActionGroup() ?: return
toolbar = JupyterToolbar(actionGroup).apply {
targetComponent = editor.contentComponent
}
toolbar = JupyterToolbar(actionGroup, editor.contentComponent)
add(toolbar)
toolbar?.let {
it.bounds = calculateToolbarBounds()
editor.contentComponent.revalidate()
editor.contentComponent.repaint()
}
adjustToolbarBounds()
addComponentListeners()
}
@@ -44,38 +38,38 @@ class NotebookBelowLastCellPanelNew(val editor: EditorImpl) : JPanel(GridBagLayo
this.addComponentListener(object : ComponentAdapter() {
override fun componentResized(e: ComponentEvent?) {
super.componentResized(e)
toolbar?.let {
it.bounds = calculateToolbarBounds()
revalidate()
repaint()
}
adjustToolbarBounds()
}
override fun componentShown(e: ComponentEvent?) {
super.componentShown(e)
toolbar?.let {
it.bounds = calculateToolbarBounds()
revalidate()
repaint()
}
adjustToolbarBounds()
}
})
}
private fun calculateToolbarBounds(): Rectangle {
val toolbarPreferredSize = toolbar?.preferredSize ?: Dimension(0, 0)
val newBounds = JupyterToolbarManager.calculateToolbarBounds(editor,
this,
toolbarPreferredSize,
extraYOffset = 30)
return newBounds
}
private fun createActionGroup(): ActionGroup? {
return CustomActionsSchema.getInstance().getCorrectedAction(ACTION_GROUP_ID) as? ActionGroup
}
private fun adjustToolbarBounds() {
toolbar?.let { tb ->
val toolbarPreferredSize = tb.preferredSize
val xOffset = (this.width - toolbarPreferredSize.width) / 2
val editorComponent = editor.contentComponent
val panelLocationInEditor = SwingUtilities.convertPoint(this, Point(0, 0), editorComponent)
val xCoordinate = panelLocationInEditor.x + xOffset
val yCoordinate = panelLocationInEditor.y + Y_OFFSET
tb.bounds = Rectangle(xCoordinate, yCoordinate, toolbarPreferredSize.width, toolbarPreferredSize.height)
revalidate()
repaint()
}
}
companion object {
private val Y_OFFSET = JBUIScale.scale(30)
const val ACTION_GROUP_ID = "Jupyter.BelowCellNewPanel"
}
}