IDEA-287497 Support running current file in the additional running options popup
[idea/community.git] / platform / execution-impl / src / com / intellij / execution / ui / RedesignedRunWidget.kt
1 // Copyright 2000-2022 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license.
2 package com.intellij.execution.ui
3
4 import com.intellij.execution.Executor
5 import com.intellij.execution.RunManager
6 import com.intellij.execution.RunnerAndConfigurationSettings
7 import com.intellij.execution.actions.RunConfigurationsComboBoxAction
8 import com.intellij.execution.impl.ExecutionManagerImpl
9 import com.intellij.icons.AllIcons
10 import com.intellij.ide.IdeBundle
11 import com.intellij.openapi.actionSystem.*
12 import com.intellij.openapi.actionSystem.ex.CustomComponentAction
13 import com.intellij.openapi.actionSystem.impl.ActionButtonWithText
14 import com.intellij.openapi.actionSystem.impl.ActionToolbarImpl
15 import com.intellij.openapi.actionSystem.impl.IdeaActionButtonLook
16 import com.intellij.openapi.project.DumbAware
17 import com.intellij.openapi.project.Project
18 import com.intellij.openapi.ui.popup.JBPopupFactory
19 import com.intellij.openapi.util.IconLoader
20 import com.intellij.openapi.util.NlsActions
21 import com.intellij.openapi.wm.ToolWindowId
22 import com.intellij.ui.DeferredIcon
23 import com.intellij.ui.JBColor
24 import com.intellij.ui.scale.JBUIScale
25 import com.intellij.util.ui.JBInsets
26 import com.intellij.util.ui.JBUI
27 import com.intellij.util.ui.JBUI.Borders
28 import com.intellij.util.ui.JBUI.CurrentTheme.CustomFrameDecorations
29 import com.intellij.util.ui.JBValue
30 import com.intellij.util.ui.UIUtil
31 import java.awt.*
32 import java.awt.event.InputEvent
33 import java.awt.geom.Area
34 import java.awt.geom.Rectangle2D
35 import java.awt.geom.RoundRectangle2D
36 import javax.swing.*
37
38 internal fun createRunToolbarWithoutStop(project: Project): ActionToolbar {
39   val actionGroup = DefaultActionGroup()
40   actionGroup.add(RunToolboxWrapper(project))
41   actionGroup.addSeparator()
42   actionGroup.add(StopWithDropDownAction())
43   return ActionManager.getInstance().createActionToolbar(
44     ActionPlaces.MAIN_TOOLBAR,
45     actionGroup,
46     true
47   ).apply {
48     targetComponent = null
49     setReservePlaceAutoPopupIcon(false)
50     layoutPolicy = ActionToolbar.NOWRAP_LAYOUT_POLICY
51     if (this is ActionToolbarImpl) {
52       isOpaque = false
53       setSeparatorCreator { Box.createHorizontalStrut(JBUIScale.scale(8)) }
54       setMinimumButtonSize(JBUI.size(36, 30))
55       setActionButtonBorder(Borders.empty())
56     }
57   }
58 }
59
60 private fun createRunActionToolbar(project: Project): ActionToolbar {
61   val actionGroup = DefaultActionGroup()
62
63   actionGroup.add(RunConfigurationSelector())
64   actionGroup.addSeparator()
65   val topLevelRunActions = listOf(IdeActions.ACTION_DEFAULT_RUNNER, IdeActions.ACTION_DEFAULT_DEBUGGER).mapNotNull {
66     ActionManager.getInstance().getAction(it)
67   }
68   actionGroup.addAll(topLevelRunActions)
69   actionGroup.add(OtherRunOptions())
70
71   return ActionManager.getInstance().createActionToolbar(
72     ActionPlaces.MAIN_TOOLBAR,
73     actionGroup,
74     true
75   ).apply {
76     targetComponent = null
77     setReservePlaceAutoPopupIcon(false)
78     layoutPolicy = ActionToolbar.NOWRAP_LAYOUT_POLICY
79     if (this is ActionToolbarImpl) {
80       isOpaque = false
81       setMinimumButtonSize(JBUI.size(36, 30))
82       setActionButtonBorder(Borders.empty())
83       setSeparatorCreator { createSeparator() }
84       setCustomButtonLook(RunWidgetButtonLook(project))
85     }
86   }
87 }
88
89 private class RunToolboxWrapper(private val project: Project) : AnAction(), CustomComponentAction {
90   private var remembered: RunnerAndConfigurationSettings? = null
91   private var isRunning = false
92
93   override fun actionPerformed(e: AnActionEvent): Unit = error("Should not be invoked")
94
95   override fun createCustomComponent(presentation: Presentation, place: String): JComponent = createRunActionToolbar(project).component
96
97   override fun updateCustomComponent(component: JComponent, presentation: Presentation) {
98     val selectedConfiguration: RunnerAndConfigurationSettings? = RunManager.getInstance(project).selectedConfiguration
99     val someRunning = isCurrentConfigurationRunning(project)
100     if (remembered != selectedConfiguration || someRunning != isRunning) {
101       component.repaint()
102     }
103     remembered = selectedConfiguration
104     isRunning = someRunning
105   }
106 }
107
108 private class RunWidgetButtonLook(private val project: Project) : IdeaActionButtonLook() {
109   override fun getStateBackground(component: JComponent, state: Int): Color {
110
111     val color = if (isCurrentConfigurationRunning(project))
112       JBColor.namedColor("Green5", 0x599E5E)
113     else
114       JBColor.namedColor("Blue5",0x3369D6)
115
116     return when (state) {
117       ActionButtonComponent.NORMAL -> color
118       ActionButtonComponent.PUSHED -> color.addAlpha(0.9)
119       else -> color.addAlpha(0.9)
120     }
121   }
122
123   override fun paintBackground(g: Graphics, component: JComponent, @ActionButtonComponent.ButtonState state: Int) {
124     val rect = Rectangle(component.size)
125     val color = getStateBackground(component, state)
126
127     val g2 = g.create() as Graphics2D
128
129     try {
130       g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON)
131       g2.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_NORMALIZE)
132       g2.color = color
133       val arc = buttonArc.float
134       val width = rect.width
135       val height = rect.height
136
137       val shape = when (component) {
138         component.parent?.components?.lastOrNull() -> {
139           val shape1 = RoundRectangle2D.Float(rect.x.toFloat(), rect.y.toFloat(), width.toFloat(), height.toFloat(), arc, arc)
140           val shape2 = Rectangle2D.Float(rect.x.toFloat(), rect.y.toFloat(), arc, height.toFloat())
141           Area(shape1).also { it.add(Area(shape2)) }
142         }
143         component.parent?.components?.get(0) -> {
144           val shape1 = RoundRectangle2D.Float(rect.x.toFloat(), rect.y.toFloat(), width.toFloat(), height.toFloat(), arc, arc)
145           val shape2 = Rectangle2D.Float((rect.x + width).toFloat() - arc, rect.y.toFloat(), arc, height.toFloat())
146           Area(shape1).also { it.add(Area(shape2)) }
147         }
148         else -> {
149           Rectangle2D.Float(rect.x.toFloat(), rect.y.toFloat(), width.toFloat(), height.toFloat())
150         }
151       }
152
153       g2.fill(shape)
154     }
155     finally {
156       g2.dispose()
157     }
158   }
159
160
161   override fun paintIcon(g: Graphics, actionButton: ActionButtonComponent, icon: Icon, x: Int, y: Int) {
162     if (icon.iconWidth == 0 || icon.iconHeight == 0) {
163       return
164     }
165     // TODO: need more magic about icons
166     var targetIcon = icon
167     if (targetIcon is DeferredIcon) {
168       targetIcon = targetIcon.evaluate()
169
170     } else {
171       targetIcon = IconLoader.filterIcon(icon, { UIUtil.GrayFilter(100, 100, 100) }, null)
172     }
173     super.paintIcon(g, actionButton, targetIcon, x, y)
174   }
175
176   override fun paintLookBorder(g: Graphics, rect: Rectangle, color: Color) {}
177   override fun getButtonArc(): JBValue = JBValue.Float(8f)
178 }
179
180
181 private abstract class TogglePopupAction : ToggleAction {
182
183   constructor()
184
185   constructor(@NlsActions.ActionText text: String?,
186               @NlsActions.ActionDescription description: String?,
187               icon: Icon?) : super(text, description, icon)
188
189   private var selectedState: Boolean = false
190
191   override fun isSelected(e: AnActionEvent): Boolean {
192     return selectedState
193   }
194
195   override fun setSelected(e: AnActionEvent, state: Boolean) {
196     selectedState = state
197     if (!selectedState) return
198     val component = e.inputEvent?.component as? JComponent ?: return
199     val actionGroup = getActionGroup(e) ?: return
200     val function = { selectedState = false }
201     val popup = JBPopupFactory.getInstance().createActionGroupPopup(
202       null, actionGroup, e.dataContext, false, false, false, function, 30, null)
203     popup.showUnderneathOf(component)
204   }
205
206   abstract fun getActionGroup(e: AnActionEvent): ActionGroup?
207 }
208
209 private class OtherRunOptions : TogglePopupAction(
210   IdeBundle.message("show.options.menu"), IdeBundle.message("show.options.menu"), AllIcons.Actions.More
211 ), DumbAware {
212   override fun getActionGroup(e: AnActionEvent): ActionGroup? {
213     val project = e.project ?: return null
214     val selectedConfiguration = RunManager.getInstance(project).selectedConfiguration
215     val executorFilter: (Executor) -> Boolean = {
216       // Cannot use DefaultDebugExecutor.EXECUTOR_ID because of module dependencies
217       it.id != ToolWindowId.RUN && it.id != ToolWindowId.DEBUG
218     }
219
220     if (selectedConfiguration != null) {
221       return RunConfigurationsComboBoxAction.SelectConfigAction(selectedConfiguration, project, executorFilter)
222     }
223     if (RunConfigurationsComboBoxAction.hasRunCurrentFileItem(project)) {
224       return RunConfigurationsComboBoxAction.RunCurrentFileAction(executorFilter)
225     }
226     return ActionGroup.EMPTY_GROUP
227   }
228 }
229
230 private class RunConfigurationSelector : TogglePopupAction(), CustomComponentAction, DumbAware {
231   override fun setSelected(e: AnActionEvent, state: Boolean) {
232     if (e.inputEvent.modifiersEx and InputEvent.SHIFT_DOWN_MASK != 0) {
233       ActionManager.getInstance().getAction("editRunConfigurations").actionPerformed(e)
234       return
235     }
236     super.setSelected(e, state)
237   }
238
239   override fun getActionGroup(e: AnActionEvent): ActionGroup? {
240     val component = e.inputEvent?.component as? JComponent ?: return null
241     val action = ActionManager.getInstance().getAction("RunConfiguration")
242     val runConfigAction = action as? RunConfigurationsComboBoxAction ?: return null
243     return runConfigAction.createPopupActionGroupOpen(component)
244   }
245
246   override fun update(e: AnActionEvent) {
247     super.update(e)
248     val action = ActionManager.getInstance().getAction("RunConfiguration")
249     val runConfigAction = action as? RunConfigurationsComboBoxAction ?: return
250     runConfigAction.update(e)
251   }
252
253   override fun displayTextInToolbar(): Boolean {
254     return true
255   }
256
257   override fun createCustomComponent(presentation: Presentation, place: String): JComponent {
258     return object : ActionButtonWithText(this, presentation, place, JBUI.size(90, 30)){
259       override fun getMargins(): Insets = JBInsets.create(0, 10)
260       override fun iconTextSpace(): Int = JBUI.scale(6)
261     }.also {
262       it.foreground = Color.WHITE
263       it.setHorizontalTextAlignment(SwingConstants.LEFT)
264     }
265   }
266 }
267
268 private fun createSeparator(): JComponent {
269   return JPanel().also {
270     it.preferredSize = JBUI.size(1, 30)
271     it.background = JBColor.namedColor("MainToolbar.background", CustomFrameDecorations.titlePaneBackground())
272   }
273 }
274
275 private fun isCurrentConfigurationRunning(project: Project): Boolean {
276   val selectedConfiguration: RunnerAndConfigurationSettings = RunManager.getInstance(project).selectedConfiguration ?: return false
277   val runningDescriptors = ExecutionManagerImpl.getInstance(project).getRunningDescriptors { it === selectedConfiguration }
278   return !runningDescriptors.isEmpty()
279 }
280
281 private fun Color.addAlpha(alpha: Double): Color {
282   return JBColor.lazy { Color(red, green, blue, (255 * alpha).toInt()) }
283 }