diff --git a/images/resources/messages/ImagesBundle.properties b/images/resources/messages/ImagesBundle.properties index 70e4baaa41bb..2d33c0c4913f 100644 --- a/images/resources/messages/ImagesBundle.properties +++ b/images/resources/messages/ImagesBundle.properties @@ -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 \ No newline at end of file +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 \ No newline at end of file diff --git a/images/src/org/intellij/images/scientific/action/DisplaySingleChannelAction.kt b/images/src/org/intellij/images/scientific/action/DisplaySingleChannelAction.kt new file mode 100644 index 000000000000..192895286d85 --- /dev/null +++ b/images/src/org/intellij/images/scientific/action/DisplaySingleChannelAction.kt @@ -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 + } +} \ No newline at end of file diff --git a/images/src/org/intellij/images/scientific/action/ImageOperationsActionGroup.kt b/images/src/org/intellij/images/scientific/action/ImageOperationsActionGroup.kt index fadaaeada50b..b855e2fb4004 100644 --- a/images/src/org/intellij/images/scientific/action/ImageOperationsActionGroup.kt +++ b/images/src/org/intellij/images/scientific/action/ImageOperationsActionGroup.kt @@ -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?, - value: Any?, - index: Int, - isSelected: Boolean, - cellHasFocus: Boolean, - ): JComponent { + override fun getListCellRendererComponent(list: JList?, 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") } diff --git a/images/src/org/intellij/images/scientific/statistics/ScientificImageActionsCollector.kt b/images/src/org/intellij/images/scientific/statistics/ScientificImageActionsCollector.kt index 39d264e526af..7997554b9f78 100644 --- a/images/src/org/intellij/images/scientific/statistics/ScientificImageActionsCollector.kt +++ b/images/src/org/intellij/images/scientific/statistics/ScientificImageActionsCollector.kt @@ -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) + } } \ No newline at end of file