src/org/jetbrains/r/debugger/RXVariablesView.kt (354 lines of code) (raw):
/*
* Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
*/
package org.jetbrains.r.debugger
import com.intellij.icons.AllIcons
import com.intellij.ide.DataManager
import com.intellij.idea.ActionsBundle
import com.intellij.openapi.Disposable
import com.intellij.openapi.actionSystem.*
import com.intellij.openapi.actionSystem.ex.ActionUtil
import com.intellij.openapi.actionSystem.impl.ActionToolbarImpl
import com.intellij.openapi.ui.Messages
import com.intellij.openapi.util.Disposer
import com.intellij.openapi.util.EmptyRunnable
import com.intellij.r.psi.RBundle
import com.intellij.r.psi.RPluginCoroutineScope
import com.intellij.r.psi.rinterop.RVar
import com.intellij.ui.AppUIUtil
import com.intellij.ui.ClickListener
import com.intellij.ui.DoubleClickListener
import com.intellij.ui.ListenerUtil
import com.intellij.util.Alarm
import com.intellij.util.concurrency.ThreadingAssertions
import com.intellij.util.containers.ContainerUtil
import com.intellij.util.ui.UIUtil.getMultiClickInterval
import com.intellij.util.ui.tree.TreeUtil
import com.intellij.xdebugger.XDebugSession
import com.intellij.xdebugger.XExpression
import com.intellij.xdebugger.XNamedTreeNode
import com.intellij.xdebugger.frame.XStackFrame
import com.intellij.xdebugger.impl.actions.XDebuggerActions
import com.intellij.xdebugger.impl.frame.XVariablesViewBase
import com.intellij.xdebugger.impl.frame.XWatchesView
import com.intellij.xdebugger.impl.frame.actions.XMoveWatchDown
import com.intellij.xdebugger.impl.frame.actions.XMoveWatchUp
import com.intellij.xdebugger.impl.frame.actions.XWatchesTreeActionBase
import com.intellij.xdebugger.impl.messages.XDebuggerImplBundle
import com.intellij.xdebugger.impl.ui.DebuggerUIUtil
import com.intellij.xdebugger.impl.ui.tree.XDebuggerTree
import com.intellij.xdebugger.impl.ui.tree.XDebuggerTreeState
import com.intellij.xdebugger.impl.ui.tree.nodes.*
import com.intellij.r.psi.actions.RDumbAwareBgtAction
import org.jetbrains.r.actions.RDumbAwareBgtToggleAction
import org.jetbrains.r.console.RConsoleViewImpl
import org.jetbrains.r.console.RDebuggerPanel
import org.jetbrains.r.packages.RequiredPackage
import org.jetbrains.r.packages.RequiredPackageInstaller
import org.jetbrains.r.run.debug.stack.RXDebuggerEvaluator
import org.jetbrains.r.run.debug.stack.RXStackFrame
import org.jetbrains.r.run.debug.stack.RXVariableViewSettings
import org.jetbrains.r.run.visualize.RImportBaseDataDialog
import org.jetbrains.r.run.visualize.RImportCsvDataDialog
import org.jetbrains.r.run.visualize.RImportExcelDataDialog
import com.intellij.r.psi.visualization.ui.ToolbarUtil
import java.awt.BorderLayout
import java.awt.event.*
import javax.swing.SwingUtilities
import javax.swing.event.TreeSelectionListener
class RXVariablesView(private val console: RConsoleViewImpl, private val debuggerPanel: RDebuggerPanel)
: XVariablesViewBase(console.rInterop.project, RDebuggerEditorsProvider, null), XWatchesView, DataProvider {
var stackFrame: RXStackFrame? = null
set(frame) {
field = frame
AppUIUtil.invokeLaterIfProjectAlive(console.project) {
if (frame == null) {
clear()
} else {
buildTreeAndRestoreState(frame)
}
}
}
private var rootNode: WatchesRootNode? = null
val settings = RXVariableViewSettings()
init {
DataManager.registerDataProvider(panel, this)
installActions()
installEditListeners()
installShowListener()
installToolbar()
}
override fun clear() {
tree.sourcePosition = null
val root = createNewRootNode(null)
root.setInfoMessage(RBundle.message("debugger.frame.not.available"), null)
super.clear()
}
override fun doCreateNewRootNode(stackFrame: XStackFrame?, state: XDebuggerTreeState?): XValueContainerNode.Root<*> {
val watchExpressions = rootNode?.watchChildren.orEmpty().map { it.expression }
val node = object : WatchesRootNode(tree, this, watchExpressions, stackFrame, true, state) {
override fun clearChildren() {
this@RXVariablesView.stackFrame?.resetOffset()
super.clearChildren()
}
}
rootNode = node
return node
}
override fun processSessionEvent(event: SessionEvent, session: XDebugSession) {
}
override fun removeWatches(nodes: List<XDebuggerTreeNode>?) {
ThreadingAssertions.assertEventDispatchThread()
val rootNode = rootNode ?: return
rootNode.removeChildren(nodes)
}
override fun removeAllWatches() {
ThreadingAssertions.assertEventDispatchThread()
val rootNode = rootNode ?: return
rootNode.removeAllChildren()
}
override fun addWatchExpression(expression: XExpression, index: Int, navigateToWatchNode: Boolean) {
addWatchExpression(expression, index, navigateToWatchNode, false)
}
private fun addWatchExpression(expression: XExpression, index: Int, navigateToWatchNode: Boolean, noDuplicates: Boolean) {
ThreadingAssertions.assertEventDispatchThread()
val rootNode = rootNode ?: return
if (noDuplicates) {
val child = rootNode.watchChildren.firstOrNull { it.expression == expression }
if (child != null) {
TreeUtil.selectNode(tree, child)
return
}
}
rootNode.addWatchExpression(stackFrame, expression, index, navigateToWatchNode)
}
fun moveWatchUp(node: WatchNode) {
rootNode?.moveUp(node)
}
fun moveWatchDown(node: WatchNode) {
rootNode?.moveDown(node)
}
private fun installActions() {
DebuggerUIUtil.registerActionOnComponent(XDebuggerActions.XNEW_WATCH, tree, this)
DebuggerUIUtil.registerActionOnComponent(XDebuggerActions.XREMOVE_WATCH, tree, this)
DebuggerUIUtil.registerActionOnComponent(XDebuggerActions.XCOPY_WATCH, tree, this)
DebuggerUIUtil.registerActionOnComponent(XDebuggerActions.XEDIT_WATCH, tree, this)
ActionUtil.wrap(XDebuggerActions.XNEW_WATCH).registerCustomShortcutSet(CommonShortcuts.getNew(), tree)
ActionUtil.wrap(XDebuggerActions.XREMOVE_WATCH).registerCustomShortcutSet(CommonShortcuts.getDelete(), tree)
}
private fun installEditListeners() {
val watchTree = tree
val quitePeriod = Alarm(RPluginCoroutineScope.getScope(console.project))
val editAlarm = Alarm(RPluginCoroutineScope.getScope(console.project))
val mouseListener = object : ClickListener() {
override fun onClick(event: MouseEvent, clickCount: Int): Boolean {
if (!SwingUtilities.isLeftMouseButton(event) || event.isShiftDown || event.isAltDown || event.isControlDown || event.isMetaDown) {
return false
}
if (!isAboveSelectedItem(event, watchTree, false) || clickCount > 1) {
editAlarm.cancelAllRequests()
return false
}
val editWatchAction = ActionManager.getInstance().getAction(XDebuggerActions.XEDIT_WATCH)
val presentation = editWatchAction.getTemplatePresentation().clone()
val context = DataManager.getInstance().getDataContext(watchTree)
val actionEvent = AnActionEvent.createEvent(context, presentation, "WATCH_TREE", ActionUiKind.TOOLBAR, null)
val runnable = { editWatchAction.actionPerformed(actionEvent) }
if (editAlarm.isEmpty && quitePeriod.isEmpty) {
editAlarm.addRequest(runnable, getMultiClickInterval())
} else {
editAlarm.cancelAllRequests()
}
return false
}
}
val mouseEmptySpaceListener = object : DoubleClickListener() {
override fun onDoubleClick(event: MouseEvent): Boolean {
if (!isAboveSelectedItem(event, watchTree, true)) {
rootNode?.addNewWatch()
return true
}
return false
}
}
ListenerUtil.addClickListener(watchTree, mouseListener)
ListenerUtil.addClickListener(watchTree, mouseEmptySpaceListener)
val focusListener = object : FocusListener {
override fun focusGained(e: FocusEvent) {
quitePeriod.addRequest(EmptyRunnable.getInstance(), getMultiClickInterval())
}
override fun focusLost(e: FocusEvent) {
editAlarm.cancelAllRequests()
}
}
ListenerUtil.addFocusListener(watchTree, focusListener)
val selectionListener = TreeSelectionListener { quitePeriod.addRequest(EmptyRunnable.getInstance(), getMultiClickInterval()) }
watchTree.addTreeSelectionListener(selectionListener)
Disposer.register(this, Disposable {
ListenerUtil.removeClickListener(watchTree, mouseListener)
ListenerUtil.removeClickListener(watchTree, mouseEmptySpaceListener)
ListenerUtil.removeFocusListener(watchTree, focusListener)
watchTree.removeTreeSelectionListener(selectionListener)
})
}
private fun installShowListener() {
panel.addComponentListener(object : ComponentListener {
override fun componentMoved(p0: ComponentEvent?) { }
override fun componentResized(p0: ComponentEvent?) { }
override fun componentHidden(p0: ComponentEvent?) { }
override fun componentShown(p0: ComponentEvent?) {
rootNode?.computeWatches()
}
})
}
private fun installToolbar() {
val actions = DefaultActionGroup(
ActionManager.getInstance().getAction("XDebugger.NewWatch"),
ActionManager.getInstance().getAction("XDebugger.RemoveWatch"),
object : XMoveWatchUp() {
init {
this.templatePresentation.text = XDebuggerImplBundle.message("action.XDebugger.MoveWatchUp.text")
}
override fun perform(e: AnActionEvent, tree: XDebuggerTree, watchesView: XWatchesView) {
(watchesView as? RXVariablesView)?.moveWatchUp(
ContainerUtil.getFirstItem(XWatchesTreeActionBase.getSelectedNodes(tree, WatchNodeImpl::class.java)))
}
},
object : XMoveWatchDown() {
init {
this.templatePresentation.text = XDebuggerImplBundle.message("action.XDebugger.MoveWatchDown.text")
}
override fun perform(e: AnActionEvent, tree: XDebuggerTree, watchesView: XWatchesView) {
(watchesView as? RXVariablesView)?.moveWatchDown(
ContainerUtil.getFirstItem(XWatchesTreeActionBase.getSelectedNodes(tree, WatchNodeImpl::class.java)))
}
},
ActionManager.getInstance().getAction("XDebugger.CopyWatch")
)
actions.addSeparator()
actions.addAction(object : RDumbAwareBgtAction(RBundle.message("variable.view.clear.environment.action.text"), null, AllIcons.Actions.GC) {
override fun actionPerformed(e: AnActionEvent) {
val environment = stackFrame?.environment ?: return
val yesNo = Messages.showYesNoDialog(e.project, RBundle.message("variable.view.clear.environment.message"),
RBundle.message("variable.view.clear.environment.action.text"), null)
if (yesNo == Messages.YES) {
console.rInterop.executeTask {
console.rInterop.clearEnvironment(environment)
console.executeActionHandler.fireCommandExecuted()
}
}
}
override fun update(e: AnActionEvent) {
e.presentation.isEnabled = !console.isRunningCommand
}
})
actions.addAction(createSettingsActionGroup())
actions.addAction(object : RDumbAwareBgtAction(ActionsBundle.message("action.EvaluateExpression.text"), null,
AllIcons.Debugger.EvaluateExpression) {
override fun actionPerformed(e: AnActionEvent) {
RDebuggerEvaluateHandler.perform(console.project, RXDebuggerEvaluator(stackFrame ?: return), e.dataContext)
}
})
actions.addAction(createImportActionGroup())
val toolbar = ActionManager.getInstance().createActionToolbar(ActionPlaces.TOOLBAR, actions, false) as ActionToolbarImpl
toolbar.setTargetComponent(tree)
panel.add(toolbar.component, BorderLayout.WEST)
}
private fun createImportActionGroup(): ActionGroup {
val project = console.project
val interop = console.rInterop
val actions = listOf(
Separator(RBundle.message("group.org.jetbrains.r.run.visualize.actions.RImportDataContextActionGroup.text")),
ToolbarUtil.createAnActionButton<RImportBaseDataAction> {
RImportBaseDataDialog.show(project, interop, project)
},
createPackageDependentAction<RImportCsvDataAction>(IMPORT_CSV_REQUIREMENTS) {
RImportCsvDataDialog.show(project, interop, project)
},
createPackageDependentAction<RImportExcelDataAction>(IMPORT_EXCEL_REQUIREMENTS) {
RImportExcelDataDialog.show(project, interop, project)
}
)
return DefaultActionGroup(RBundle.message("import.data.action.group.name"), actions).apply {
templatePresentation.icon = AllIcons.ToolbarDecorator.Import
isPopup = true
}
}
private inline fun <reified A : AnAction>createPackageDependentAction(packageNames: List<String>, noinline onClick: () -> Unit): AnAction {
val requirements = packageNames.map { RequiredPackage(it) }
val holder = object : ToolbarUtil.ActionHolder {
private val missing: List<RequiredPackage>?
get() {
val installer = RequiredPackageInstaller.getInstance(console.project)
return installer.getMissingPackagesOrNull(requirements)
}
override val id = A::class.qualifiedName ?: ""
override val canClick: Boolean
get() = missing?.isEmpty() == true
override fun onClick() {
onClick()
}
override fun getHintForDisabled(): String? {
return missing?.let { createMissingPackageMessage(it) }
}
private fun createMissingPackageMessage(missing: List<RequiredPackage>): String {
val packageString = missing.joinToString { it.toFormat(false) }
return RBundle.message("required.package.exception.message", packageString)
}
}
return ToolbarUtil.createAnActionButton(holder)
}
private fun createSettingsActionGroup(): ActionGroup {
return DefaultActionGroup(RBundle.message("variable.view.settings.text"), listOf(
object : RDumbAwareBgtToggleAction(RBundle.message("variable.view.show.hidden.variables.action.text")) {
override fun isSelected(e: AnActionEvent) = settings.showHiddenVariables
override fun setSelected(e: AnActionEvent, state: Boolean) {
if (state != settings.showHiddenVariables) {
settings.showHiddenVariables = state
debuggerPanel.refreshStackFrames()
}
}
},
object : RDumbAwareBgtToggleAction(RBundle.message("variable.view.show.classes.action.text")) {
override fun isSelected(e: AnActionEvent) = settings.showClasses
override fun setSelected(e: AnActionEvent, state: Boolean) {
if (state != settings.showClasses) {
settings.showClasses = state
debuggerPanel.refreshStackFrames()
}
}
},
object : RDumbAwareBgtToggleAction(RBundle.message("variable.view.show.size.action.text")) {
override fun isSelected(e: AnActionEvent) = settings.showSize
override fun setSelected(e: AnActionEvent, state: Boolean) {
if (state != settings.showSize) {
settings.showSize = state
debuggerPanel.refreshStackFrames()
}
}
}
)).also {
it.templatePresentation.icon = AllIcons.Actions.Show
it.isPopup = true
}
}
override fun getData(dataId: String): Any? {
return if (XWatchesView.DATA_KEY.`is`(dataId)) this else null
}
fun navigate(rVar: RVar) {
val proto = rVar.ref.proto
if (proto.hasMember() && proto.member.env.hasGlobalEnv()) {
val target = rootNode?.children?.filterIsInstance<XNamedTreeNode>()?.firstOrNull { it.name == rVar.name } ?: return
TreeUtil.selectNode(tree, target)
}
}
companion object {
private val IMPORT_EXCEL_REQUIREMENTS = listOf("readxl")
private val IMPORT_CSV_REQUIREMENTS = listOf("readr")
private fun isAboveSelectedItem(event: MouseEvent, watchTree: XDebuggerTree, fullWidth: Boolean): Boolean {
val bounds = watchTree.getRowBounds(watchTree.leadSelectionRow) ?: return false
if (fullWidth) {
bounds.x = 0
}
bounds.width = watchTree.width
return bounds.contains(event.point)
}
}
}