1 // Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
2 package com.intellij.execution.runToolbar
4 import com.intellij.CommonBundle
5 import com.intellij.execution.IS_RUN_MANAGER_INITIALIZED
6 import com.intellij.execution.RunManager
7 import com.intellij.execution.RunManagerListener
8 import com.intellij.execution.RunnerAndConfigurationSettings
9 import com.intellij.execution.compound.CompoundRunConfiguration
10 import com.intellij.execution.impl.ExecutionManagerImpl
11 import com.intellij.execution.runToolbar.data.*
12 import com.intellij.execution.runners.ExecutionEnvironment
13 import com.intellij.ide.ActivityTracker
14 import com.intellij.lang.LangBundle
15 import com.intellij.openapi.components.service
16 import com.intellij.openapi.diagnostic.Logger
17 import com.intellij.openapi.project.Project
18 import com.intellij.openapi.ui.Messages
19 import com.intellij.openapi.util.CheckedDisposable
20 import com.intellij.openapi.util.Disposer
21 import com.intellij.ui.AppUIUtil
22 import com.intellij.util.messages.Topic
24 import javax.swing.SwingUtilities
26 class RunToolbarSlotManager(val project: Project) {
28 private val LOG = Logger.getInstance(RunToolbarSlotManager::class.java)
29 fun getInstance(project: Project): RunToolbarSlotManager = project.service()
33 val RUN_TOOLBAR_SLOT_CONFIGURATION_MAP_TOPIC = Topic("RunToolbarWidgetSlotConfigurationMapChanged",
34 RWSlotsConfigurationListener::class.java)
37 private val runToolbarSettings = RunToolbarSettings.getInstance(project)
39 internal val slotListeners = RWSlotController()
40 internal val activeListener = RWAddedController()
41 internal val stateListeners = RWStateController()
43 internal var mainSlotData = SlotDate(UUID.randomUUID().toString())
45 val activeProcesses = RWActiveProcesses()
46 private val dataIds = mutableListOf<String>()
48 private val slotsData = mutableMapOf<String, SlotDate>()
50 private var activeDisposable: CheckedDisposable? = null
52 private val processController = RWProcessController(project)
54 internal var active: Boolean = false
56 if (field == value) return
61 if (RunToolbarProcess.logNeeded) LOG.info(
62 "ACTIVE SM settings: new on top ${runToolbarSettings.getMoveNewOnTop()}; update by selected ${getUpdateMainBySelected()} RunToolbar")
65 val disp = Disposer.newCheckedDisposable()
66 Disposer.register(project, disp)
67 activeDisposable = disp
69 val settingsData = runToolbarSettings.getConfigurations()
70 val slotOrder = settingsData.first
71 val configurations = settingsData.second
73 slotOrder.filter { configurations[it] != null }.forEachIndexed { index, s ->
75 mainSlotData.updateId(s)
76 mainSlotData.configuration = configurations[s]
77 slotsData[mainSlotData.id] = mainSlotData
80 addSlot(configurations[s], s)
84 if (RunToolbarProcess.logNeeded) LOG.info("SM restoreRunConfigurations: ${configurations.values} RunToolbar")
86 val con = project.messageBus.connect(disp)
88 con.subscribe(RunManagerListener.TOPIC, object : RunManagerListener {
89 override fun runConfigurationSelected(settings: RunnerAndConfigurationSettings?) {
90 if (!getUpdateMainBySelected() || mainSlotData.configuration == settings) return
92 mainSlotData.environment?.let {
93 val slot = addSlot(settings)
94 if (RunToolbarProcess.logNeeded) LOG.info("SM runConfigurationSelected: $settings first slot added RunToolbar")
97 mainSlotData.configuration = settings
98 if (RunToolbarProcess.logNeeded) LOG.info("SM runConfigurationSelected: $settings change main configuration RunToolbar")
103 override fun runConfigurationRemoved(settings: RunnerAndConfigurationSettings) {
105 slotsData.filter { it.value == settings && it.value.environment == null }.forEach {
107 it.value.configuration = RunManager.getInstance(project).selectedConfiguration
115 val executions = processController.getActiveExecutions()
116 executions.filter { it.isRunning() == true }.forEach { addNewProcess(it) }
117 activeListener.enabled()
121 SwingUtilities.invokeLater {
122 ActivityTracker.getInstance().inc()
126 activeDisposable?.let {
129 activeDisposable = null
132 activeListener.disabled()
134 if (RunToolbarProcess.logNeeded) LOG.info(
135 "INACTIVE SM RunToolbar")
138 slotListeners.rebuildPopup()
139 publishConfigurations(getConfigurationMap())
142 private fun getUpdateMainBySelected(): Boolean {
143 return runToolbarSettings.getUpdateMainBySelected()
146 private fun getMoveNewOnTop(executionEnvironment: ExecutionEnvironment): Boolean {
147 if (!runToolbarSettings.getMoveNewOnTop()) return false
148 val suppressValue = executionEnvironment.getUserData(RunToolbarData.RUN_TOOLBAR_SUPPRESS_MAIN_SLOT_USER_DATA_KEY) ?: false
149 return !suppressValue
152 private fun clear() {
156 slotsData[mainSlotData.id] = mainSlotData
158 activeProcesses.clear()
159 state = RWSlotManagerState.INACTIVE
163 private fun traceState() {
164 if (!RunToolbarProcess.logNeeded) return
167 val ids = dataIds.indices.mapNotNull { "${it + 1}: ${slotsData[dataIds[it]]}" }.joinToString(", ")
168 LOG.info("SM state: $state" +
169 "${separator}== slots: 0: ${mainSlotData}, $ids" +
170 "${separator}== slotsData: ${slotsData.values} RunToolbar")
175 SwingUtilities.invokeLater {
176 if (project.isDisposed) return@invokeLater
178 slotsData[mainSlotData.id] = mainSlotData
180 activeListener.addListener(RunToolbarShortcutHelper(project))
182 Disposer.register(project) {
183 activeListener.clear()
184 stateListeners.clear()
185 slotListeners.clear()
190 private fun update() {
191 saveSlotsConfiguration()
194 if (!RunToolbarProcess.logNeeded) return
195 LOG.trace("!!!!!UPDATE RunToolbar")
198 internal fun startWaitingForAProcess(slotDate: RunToolbarData, settings: RunnerAndConfigurationSettings, executorId: String) {
199 slotsData.values.forEach {
200 val waitingForAProcesses = it.waitingForAProcesses
201 if (slotDate == it) {
202 waitingForAProcesses.start(project, settings, executorId)
205 if (waitingForAProcesses.isWaitingForASubProcess(settings, executorId)) {
206 waitingForAProcesses.clear()
212 internal fun getMainOrFirstActiveProcess(): RunToolbarProcess? {
213 return mainSlotData.environment?.getRunToolbarProcess() ?: activeProcesses.processes.keys.firstOrNull()
216 internal fun slotsCount(): Int {
220 private var state: RWSlotManagerState = RWSlotManagerState.INACTIVE
222 if (value == field) return
225 stateListeners.stateChanged(value)
228 private fun updateState() {
229 state = when (activeProcesses.getActiveCount()) {
230 0 -> RWSlotManagerState.INACTIVE
232 mainSlotData.environment?.let {
233 RWSlotManagerState.SINGLE_MAIN
234 } ?: RWSlotManagerState.SINGLE_PLAIN
237 mainSlotData.environment?.let {
238 RWSlotManagerState.MULTIPLE_WITH_MAIN
239 } ?: RWSlotManagerState.MULTIPLE
244 internal fun getState(): RWSlotManagerState {
248 private fun getAppropriateSettings(env: ExecutionEnvironment): Iterable<SlotDate> {
249 val sortedSlots = mutableListOf<SlotDate>()
250 sortedSlots.add(mainSlotData)
251 sortedSlots.addAll(dataIds.mapNotNull { slotsData[it] }.toList())
253 return sortedSlots.filter { it.configuration == env.runnerAndConfigurationSettings }
256 internal fun processNotStarted(env: ExecutionEnvironment) {
257 val config = env.runnerAndConfigurationSettings ?: return
259 val appropriateSettings = getAppropriateSettings(env)
260 val emptySlotsWithConfiguration = appropriateSettings.filter { it.environment == null }
262 emptySlotsWithConfiguration.map { it.waitingForAProcesses }.firstOrNull {
263 it.isWaitingForASingleProcess(config, env.executor.id)
265 slotsData.values.filter { it.configuration?.configuration is CompoundRunConfiguration }.firstOrNull { slotsData ->
266 slotsData.waitingForAProcesses.isWaitingForASubProcess(config, env.executor.id)
271 internal fun processStarted(env: ExecutionEnvironment) {
274 SwingUtilities.invokeLater {
275 ActivityTracker.getInstance().inc()
279 private fun addNewProcess(env: ExecutionEnvironment) {
280 val appropriateSettings = getAppropriateSettings(env)
281 val emptySlotsWithConfiguration = appropriateSettings.filter { it.environment == null }
284 val slot = appropriateSettings.firstOrNull { it.environment?.executionId == env.executionId }
285 ?: emptySlotsWithConfiguration.firstOrNull { slotData ->
286 env.runnerAndConfigurationSettings?.let {
287 slotData.waitingForAProcesses.isWaitingForASingleProcess(it, env.executor.id)
290 ?: emptySlotsWithConfiguration.firstOrNull()
293 addSlot(env.runnerAndConfigurationSettings)
296 slot.environment = env
297 activeProcesses.updateActiveProcesses(slotsData)
300 val isCompoundProcess = slotsData.values.filter { it.configuration?.configuration is CompoundRunConfiguration }.firstOrNull { slotsData ->
301 env.runnerAndConfigurationSettings?.let {
302 slotsData.waitingForAProcesses.checkAndUpdate(it, env.executor.id)
306 if (!isCompoundProcess) {
307 if (getMoveNewOnTop(env)) {
314 fun processTerminating(env: ExecutionEnvironment) {
315 slotsData.values.firstOrNull { it.environment?.executionId == env.executionId }?.let {
318 activeProcesses.updateActiveProcesses(slotsData)
320 SwingUtilities.invokeLater {
321 ActivityTracker.getInstance().inc()
325 fun processTerminated(executionId: Long) {
326 slotsData.values.firstOrNull { it.environment?.executionId == executionId }?.let { slotDate ->
327 val removable = slotDate.environment?.runnerAndConfigurationSettings?.let {
328 !RunManager.getInstance(project).hasSettings(it)
332 if (slotDate == mainSlotData && slotsData.size == 1) {
334 slotDate.configuration = RunManager.getInstance(project).selectedConfiguration
337 removeSlot(slotDate.id)
341 slotDate.environment = null
345 if (RunToolbarProcess.logNeeded) LOG.info("SM process stopped: $executionId RunToolbar")
346 activeProcesses.updateActiveProcesses(slotsData)
349 SwingUtilities.invokeLater {
350 ActivityTracker.getInstance().inc()
354 internal fun addAndSaveSlot(): SlotDate {
356 saveSlotsConfiguration()
360 private fun addSlot(configuration: RunnerAndConfigurationSettings? = null, id: String = UUID.randomUUID().toString()): SlotDate {
361 val slot = SlotDate(id)
362 slot.configuration = configuration
364 slotsData[slot.id] = slot
366 slotListeners.slotAdded()
371 internal fun getData(index: Int): SlotDate? {
372 return if (index >= 0 && index < dataIds.size) {
381 internal fun moveToTop(id: String) {
382 if (mainSlotData.id == id) return
384 slotsData[id]?.let { newMain ->
385 val oldMain = mainSlotData
386 mainSlotData = newMain
389 dataIds.add(0, oldMain.id)
395 internal fun removeSlot(id: String) {
396 val index = dataIds.indexOf(id)
399 if (id == mainSlotData.id) {
400 if (dataIds.isNotEmpty()) {
401 val firstSlotId = dataIds[0]
402 slotsData[firstSlotId]?.let {
405 dataIds.remove(it.id)
414 SwingUtilities.invokeLater {
415 slotListeners.slotRemoved(index)
416 ActivityTracker.getInstance().inc()
420 (if (index >= 0) getData(index) else if (mainSlotData.id == id) mainSlotData else null)?.let { slotDate ->
421 slotDate.environment?.let {
422 if (it.isRunning() != true) {
425 else if (Messages.showOkCancelDialog(
427 LangBundle.message("run.toolbar.remove.active.process.slot.message"),
428 LangBundle.message("run.toolbar.remove.active.process.slot.title", it.runnerAndConfigurationSettings?.name ?: ""),
429 LangBundle.message("run.toolbar.remove.active.process.slot.ok"),
430 CommonBundle.getCancelButtonText(),
431 Messages.getQuestionIcon()/*, object : DialogWrapper.DoNotAskOption.Adapter() {
432 override fun rememberChoice(isSelected: Boolean, exitCode: Int) {
435 }*/) == Messages.OK) {
436 it.contentToReuse?.let {
437 ExecutionManagerImpl.stopProcess(it)
445 } ?: slotListeners.rebuildPopup()
450 internal fun configurationChanged(slotId: String, configuration: RunnerAndConfigurationSettings?) {
451 AppUIUtil.invokeLaterIfProjectAlive(project) {
452 project.messageBus.syncPublisher(RUN_TOOLBAR_SLOT_CONFIGURATION_MAP_TOPIC).configurationChanged(slotId, configuration)
454 saveSlotsConfiguration()
457 private fun saveSlotsConfiguration() {
458 if (IS_RUN_MANAGER_INITIALIZED.get(project) == true) {
459 val runManager = RunManager.getInstance(project)
460 mainSlotData.configuration?.let {
461 if (runManager.hasSettings(it) &&
462 it != runManager.selectedConfiguration &&
463 mainSlotData.environment?.getRunToolbarProcess()?.isTemporaryProcess() != true) {
464 runManager.selectedConfiguration = mainSlotData.configuration
465 if (RunToolbarProcess.logNeeded) LOG.info(
466 "MANAGER saveSlotsConfiguration. change selected configuration by main: ${mainSlotData.configuration} RunToolbar")
471 val slotOrder = getSlotOrder()
472 val configurations = getConfigurationMap(slotOrder)
473 if (RunToolbarProcess.logNeeded) LOG.info("MANAGER saveSlotsConfiguration: ${configurations} RunToolbar")
475 runToolbarSettings.setConfigurations(configurations, slotOrder)
476 publishConfigurations(configurations)
479 private fun getSlotOrder(): List<String> {
480 val list = mutableListOf<String>()
481 list.add(mainSlotData.id)
486 private fun getConfigurationMap(slotOrder: List<String>): Map<String, RunnerAndConfigurationSettings?> {
487 return slotOrder.associateWith { slotsData[it]?.configuration }
490 fun getConfigurationMap(): Map<String, RunnerAndConfigurationSettings?> {
491 return getConfigurationMap(getSlotOrder())
494 private fun publishConfigurations(slotConfigurations: Map<String, RunnerAndConfigurationSettings?>) {
495 AppUIUtil.invokeLaterIfProjectAlive(project) {
496 project.messageBus.syncPublisher(RUN_TOOLBAR_SLOT_CONFIGURATION_MAP_TOPIC).slotsConfigurationChanged(slotConfigurations)
501 internal open class SlotDate(override var id: String) : RunToolbarData {
506 fun updateId(value: String) {
510 override var configuration: RunnerAndConfigurationSettings? = null
511 get() = environment?.runnerAndConfigurationSettings ?: field
513 override var environment: ExecutionEnvironment? = null
518 configuration = it.runnerAndConfigurationSettings
520 waitingForAProcesses.clear()
524 override val waitingForAProcesses = RWWaitingForAProcesses()
526 override fun clear() {
528 waitingForAProcesses.clear()
531 override fun toString(): String {
532 return "$id-${environment?.let { "$it [${it.executor.actionName} ${it.executionId}]" } ?: configuration?.configuration?.name ?: "configuration null"}"