[pycharm] PY-80467 Debugger: add separate channel view

(cherry picked from commit 6a3b002297a6892e3b5cf9fe54bfa7db3c1cabf8)

GitOrigin-RevId: 75b6f24c933df27bbefc1af1d557341bfafa4353
This commit is contained in:
ekaterina.itsenko
2025-05-01 02:54:19 +02:00
committed by intellij-monorepo-bot
parent 67c4c7c75f
commit 8372da1b56
4 changed files with 123 additions and 49 deletions

View File

@@ -95,4 +95,7 @@ image.color.mode.inverted.image=Inverted
image.color.mode.grayscale.image=Grayscale
image.color.mode.binarize.image=Binary
image.binarize.dialog.title=Set Binarization Threshold
image.color.mode.configure.actions=Configure\u2026
image.channels.mode.channel.1=Channel 1
image.channels.mode.channel.2=Channel 2
image.channels.mode.channel.3=Channel 3
image.color.mode.configure.actions=Binarization\u2026

View File

@@ -0,0 +1,78 @@
package org.intellij.images.scientific.action
import com.intellij.openapi.actionSystem.ActionUpdateThread
import com.intellij.openapi.actionSystem.AnActionEvent
import com.intellij.openapi.actionSystem.CommonDataKeys
import com.intellij.openapi.project.DumbAwareAction
import com.intellij.openapi.vfs.writeBytes
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.intellij.images.editor.ImageDocument.IMAGE_DOCUMENT_DATA_KEY
import org.intellij.images.scientific.statistics.ScientificImageActionsCollector
import org.intellij.images.scientific.utils.ScientificUtils
import org.intellij.images.scientific.utils.ScientificUtils.DEFAULT_IMAGE_FORMAT
import org.intellij.images.scientific.utils.ScientificUtils.ORIGINAL_IMAGE_KEY
import org.intellij.images.scientific.utils.ScientificUtils.ROTATION_ANGLE_KEY
import org.intellij.images.scientific.utils.launchBackground
import java.awt.image.BufferedImage
import java.io.ByteArrayOutputStream
import javax.imageio.ImageIO
class DisplaySingleChannelAction(
private val channelIndex: Int,
text: String
) : DumbAwareAction(text) {
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
override fun update(e: AnActionEvent) {
val imageFile = e.getData(CommonDataKeys.VIRTUAL_FILE)
val originalImage = imageFile?.getUserData(ORIGINAL_IMAGE_KEY)
e.presentation.isEnabled = originalImage != null && getDisplayableChannels(originalImage) > 1
}
private fun getDisplayableChannels(image: BufferedImage): Int {
val totalChannels = image.raster.numBands
return if (image.colorModel.hasAlpha()) totalChannels - 1 else totalChannels
}
override fun actionPerformed(e: AnActionEvent) {
val imageFile = e.getData(CommonDataKeys.VIRTUAL_FILE) ?: return
val originalImage = imageFile.getUserData(ORIGINAL_IMAGE_KEY) ?: return
val document = e.getData(IMAGE_DOCUMENT_DATA_KEY) ?: return
val currentAngle = imageFile.getUserData(ROTATION_ANGLE_KEY) ?: 0
ScientificImageActionsCollector.logChannelSelection(this, channelIndex)
launchBackground {
val rotatedOriginal = if (currentAngle != 0) {
ScientificUtils.rotateImage(originalImage, currentAngle)
}
else {
originalImage
}
val channelImage = displaySingleChannel(rotatedOriginal, channelIndex)
val byteArrayOutputStream = ByteArrayOutputStream()
ImageIO.write(channelImage, DEFAULT_IMAGE_FORMAT, byteArrayOutputStream)
imageFile.writeBytes(byteArrayOutputStream.toByteArray())
document.value = channelImage
}
}
private suspend fun displaySingleChannel(image: BufferedImage, channelIndex: Int): BufferedImage = withContext(Dispatchers.IO) {
val raster = image.raster
val width = image.width
val height = image.height
val channelImage = BufferedImage(width, height, BufferedImage.TYPE_BYTE_GRAY)
for (x in 0 until width) {
for (y in 0 until height) {
val value = raster.getSample(x, y, channelIndex)
channelImage.raster.setSample(x, y, 0, value)
}
}
channelImage
}
}

View File

@@ -6,10 +6,7 @@ import com.intellij.openapi.actionSystem.*
import com.intellij.openapi.actionSystem.ex.CustomComponentAction
import com.intellij.openapi.project.DumbAware
import com.intellij.openapi.ui.ComboBox
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.openapi.util.registry.Registry
import com.intellij.ui.Gray
import com.intellij.ui.JBColor
import org.intellij.images.ImagesBundle
import org.intellij.images.scientific.utils.ScientificUtils
import org.jetbrains.annotations.Nls
@@ -28,17 +25,6 @@ class ImageOperationsActionGroup : DefaultActionGroup(), CustomComponentAction,
override fun getActionUpdateThread(): ActionUpdateThread = ActionUpdateThread.BGT
override fun actionPerformed(e: AnActionEvent) {
val component = e.inputEvent?.source as? JComponent ?: return
JBPopupFactory.getInstance().createActionGroupPopup(
null,
createPopupActionGroup(),
e.dataContext,
JBPopupFactory.ActionSelectionAid.SPEEDSEARCH,
true
).showUnderneathOf(component)
}
override fun update(e: AnActionEvent) {
val shouldShowTheGroup = Registry.`is`("ide.images.sci.mode.channels.operations")
if (!shouldShowTheGroup) {
@@ -57,6 +43,9 @@ class ImageOperationsActionGroup : DefaultActionGroup(), CustomComponentAction,
addElement(INVERTED_IMAGE)
addElement(GRAYSCALE_IMAGE)
addElement(BINARIZE_IMAGE)
addElement(CHANNEL_1)
addElement(CHANNEL_2)
addElement(CHANNEL_3)
addElement(CONFIGURE_ACTIONS)
}
@@ -64,21 +53,15 @@ class ImageOperationsActionGroup : DefaultActionGroup(), CustomComponentAction,
selectedItem = selectedMode
isOpaque = false
renderer = object : DefaultListCellRenderer() {
override fun getListCellRendererComponent(
list: JList<out Any>?,
value: Any?,
index: Int,
isSelected: Boolean,
cellHasFocus: Boolean,
): JComponent {
override fun getListCellRendererComponent(list: JList<out Any>?, value: Any?, index: Int, isSelected: Boolean, cellHasFocus: Boolean): JComponent {
val component = super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus)
if (index != -1 && value == CONFIGURE_ACTIONS) {
return JSeparator().apply {
if (index != -1 && (value == BINARIZE_IMAGE || value == ORIGINAL_IMAGE || value == CHANNEL_3)) {
return JPanel().apply {
icon = if (value == CONFIGURE_ACTIONS) AllIcons.General.Settings else null
layout = BoxLayout(this, BoxLayout.Y_AXIS)
isOpaque = false
background = JBColor(Gray._200, Gray._100)
add(component)
add(JSeparator())
}
}
@@ -86,17 +69,29 @@ class ImageOperationsActionGroup : DefaultActionGroup(), CustomComponentAction,
}
}
addActionListener {
val selectedItem = selectedItem as String
if (selectedItem == CONFIGURE_ACTIONS) {
this.selectedItem = selectedMode
triggerModeAction(CONFIGURE_ACTIONS)
}
else {
selectedMode = selectedItem
triggerModeAction(selectedMode)
val selectedItem = selectedItem
when (selectedItem) {
CONFIGURE_ACTIONS -> {
this.selectedItem = selectedMode
triggerModeAction(CONFIGURE_ACTIONS)
}
in listOf(CHANNEL_1, CHANNEL_2, CHANNEL_3) -> {
val channelIndex = when (selectedItem) {
CHANNEL_1 -> 0
CHANNEL_2 -> 1
CHANNEL_3 -> 2
else -> return@addActionListener
}
ActionManager.getInstance().tryToExecute(DisplaySingleChannelAction(channelIndex, selectedItem as String), null, null, null, true)
}
else -> {
selectedMode = selectedItem as String
triggerModeAction(selectedMode)
}
}
}
}
return JPanel(FlowLayout(FlowLayout.CENTER, 0, 0)).apply {
isOpaque = false
border = null
@@ -104,16 +99,6 @@ class ImageOperationsActionGroup : DefaultActionGroup(), CustomComponentAction,
}
}
private fun createPopupActionGroup(): DefaultActionGroup {
val actionGroup = DefaultActionGroup()
actionGroup.add(RestoreOriginalImageAction())
actionGroup.add(InvertChannelsAction())
actionGroup.add(GrayscaleImageAction())
actionGroup.add(BinarizeImageAction())
actionGroup.add(ConfigureActions())
return actionGroup
}
private fun triggerModeAction(mode: String) {
val actionManager = ActionManager.getInstance()
when (mode) {
@@ -126,18 +111,20 @@ class ImageOperationsActionGroup : DefaultActionGroup(), CustomComponentAction,
}
companion object {
@Nls
private val CHANNEL_1: String = ImagesBundle.message("image.channels.mode.channel.1")
@Nls
private val CHANNEL_2: String = ImagesBundle.message("image.channels.mode.channel.2")
@Nls
private val CHANNEL_3: String = ImagesBundle.message("image.channels.mode.channel.3")
@Nls
private val ORIGINAL_IMAGE: String = ImagesBundle.message("image.color.mode.original.image")
@Nls
private val INVERTED_IMAGE: String = ImagesBundle.message("image.color.mode.inverted.image")
@Nls
private val GRAYSCALE_IMAGE: String = ImagesBundle.message("image.color.mode.grayscale.image")
@Nls
private val BINARIZE_IMAGE: String = ImagesBundle.message("image.color.mode.binarize.image")
@Nls
private val CONFIGURE_ACTIONS: String = ImagesBundle.message("image.color.mode.configure.actions")
}

View File

@@ -6,10 +6,11 @@ import com.intellij.internal.statistic.service.fus.collectors.CounterUsagesColle
import org.intellij.images.scientific.action.*
object ScientificImageActionsCollector : CounterUsagesCollector() {
private val GROUP = EventLogGroup("scientific.image.actions", 2)
private val GROUP = EventLogGroup("scientific.image.actions", 3)
private val ACTION_HANDLER_FIELD = EventFields.Class("action_handler")
private val IMAGE_FORMAT_FIELD = EventFields.String("image_format", listOf("png", "jpg", "jpeg", "bmp"))
private val IMAGE_FORMAT_FIELD = EventFields.String("image_format", listOf("png", "jpg", "jpeg", "bmp", "svg"))
private val CHANNEL_INDEX_FIELD = EventFields.Int("channel_index")
private val INVOKED_COPY_IMAGE_EVENT = GROUP.registerEvent("copy_image_action", ACTION_HANDLER_FIELD)
private val INVOKED_SAVE_IMAGE_EVENT = GROUP.registerEvent("save_image_action", ACTION_HANDLER_FIELD, IMAGE_FORMAT_FIELD)
@@ -18,6 +19,7 @@ object ScientificImageActionsCollector : CounterUsagesCollector() {
private val INVOKED_GRAYSCALE_IMAGE_EVENT = GROUP.registerEvent("grayscale_image_action", ACTION_HANDLER_FIELD)
private val INVOKED_BINARY_IMAGE_EVENT = GROUP.registerEvent("binarize_image_action", ACTION_HANDLER_FIELD)
private val INVOKED_ROTATE_IMAGE_EVENT = GROUP.registerEvent("rotate_image_action", ACTION_HANDLER_FIELD)
private val CHANNEL_SELECTION_EVENT = GROUP.registerEvent("channel_selection", ACTION_HANDLER_FIELD, CHANNEL_INDEX_FIELD)
override fun getGroup(): EventLogGroup = GROUP
@@ -48,4 +50,8 @@ object ScientificImageActionsCollector : CounterUsagesCollector() {
fun logRotateImageInvoked(action: RotateImageAction) {
INVOKED_ROTATE_IMAGE_EVENT.log(action::class.java)
}
fun logChannelSelection(action: DisplaySingleChannelAction, channelIndex: Int) {
CHANNEL_SELECTION_EVENT.log(action::class.java, channelIndex)
}
}