src/org/jetbrains/r/run/graphics/ui/RGraphicsPanelWrapper.kt (272 lines of code) (raw):

/* * Copyright 2000-2020 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.run.graphics.ui import com.intellij.openapi.Disposable import com.intellij.openapi.editor.colors.EditorColorsListener import com.intellij.openapi.editor.colors.EditorColorsManager import com.intellij.openapi.project.Project import com.intellij.openapi.util.Disposer import com.intellij.r.psi.RBundle import com.intellij.util.ui.update.MergingUpdateQueue import com.intellij.util.ui.update.Update import org.jetbrains.concurrency.runAsync import org.jetbrains.r.rendering.chunk.ChunkGraphicsManager import com.intellij.r.psi.run.graphics.* import com.intellij.r.psi.run.graphics.ui.RPlotViewer import com.intellij.r.psi.settings.RGraphicsSettings import org.jetbrains.r.visualization.inlays.components.GraphicsPanel import java.awt.BorderLayout import java.awt.Dimension import java.awt.event.ComponentAdapter import java.awt.event.ComponentEvent import java.awt.image.BufferedImage import java.nio.file.Path import javax.swing.JComponent import javax.swing.JPanel class RGraphicsPanelWrapper(project: Project, private val parent: Disposable) { private val queue = MergingUpdateQueue(RESIZE_TASK_NAME, RESIZE_TIME_SPAN, true, null, project) private val manager = ChunkGraphicsManager(project) private val graphicsPanel = GraphicsPanel(project, parent).apply { component.addComponentListener(object : ComponentAdapter() { override fun componentResized(e: ComponentEvent?) { scheduleRescalingIfNecessary() } }) showLoadingMessage() } private val plotViewer = RPlotViewer(project, parent) private val rootPanel = JPanel(BorderLayout()) private val usesViewer: Boolean get() = isStandalone && isAutoResizeEnabled @Volatile private var oldStandalone: Boolean = true val preferredImageSize: Dimension get() = GraphicsPanel.calculateImageSizeForRegion(component.size) @Volatile var localResolution: Int? = null private set @Volatile var snapshot: RSnapshot? = null private set @Volatile var plot: RPlot? = null private set @Volatile var targetResolution: Int? = null set(resolution) { if (field != resolution) { field = resolution plotViewer.resolution = resolution if (usesViewer) { localResolution = resolution } else { scheduleRescalingIfNecessary() } } } @Volatile var isStandalone: Boolean = true set(value) { if (field != value && canSwitchTo(value)) { field = value updateContent() if (usesViewer) { localResolution = plotViewer.resolution } else { localResolution = null // Note: force rescaling for builtin engine scheduleRescalingIfNecessary() } } } var isAutoResizeEnabled: Boolean get() = !graphicsPanel.isAdvancedMode set(value) { if (isAutoResizeEnabled != value) { graphicsPanel.isAdvancedMode = !value updateContent() if (value) { scheduleRescalingIfNecessary() } else if (isStandalone) { val newSize = preferredImageSize if (newSize.isValid) { graphicsPanel.showLoadingMessage(RBundle.message("graphics.panel.wrapper.waiting")) rescale(newSize, targetResolution) } } } } @Volatile var isVisible: Boolean = true set(value) { if (field != value) { field = value if (value) { scheduleRescalingIfNecessary() } } } var overlayComponent: JComponent? get() = graphicsPanel.overlayComponent set(component) { graphicsPanel.overlayComponent = component plotViewer.overlayComponent = component } val hasGraphics: Boolean get() = snapshot != null || plot != null val image: BufferedImage? get() = if (usesViewer) plotViewer.image else graphicsPanel.image val maximumSize: Dimension? get() = graphicsPanel.maximumSize val component = rootPanel init { RGraphicsSettings.addDarkModeListener(project, parent) { rescaleIfStandalone() } project.messageBus.connect(parent).subscribe(EditorColorsManager.TOPIC, EditorColorsListener { rescaleIfStandalone() }) } fun addPlot(plot: RPlot) { addGraphics(null, plot) } fun addSnapshot(snapshot: RSnapshot) { addGraphics(snapshot, null) } fun addGraphics(snapshot: RSnapshot?, plot: RPlot?) { if (snapshot == null && plot == null) { throw RuntimeException("Either snapshot or plot must be not null") } this.snapshot = snapshot this.plot = plot if (plot == null || plot.error != null) { isStandalone = false } else if (snapshot == null) { isStandalone = true } isAutoResizeEnabled = true localResolution = null updateContent() if (plot != null) { plotViewer.resolution = targetResolution plotViewer.plot = plot } if (usesViewer) { localResolution = targetResolution } else { graphicsPanel.showLoadingMessage(RBundle.message("graphics.panel.wrapper.waiting")) rescaleIfNecessary() } } fun addImage(file: Path) { snapshot = null plot = null isStandalone = false isAutoResizeEnabled = false updateContent() graphicsPanel.showImage(file) } private fun updateContent() { val targetComponent = if (usesViewer) plotViewer else graphicsPanel.component if (rootPanel.components.firstOrNull() !== targetComponent) { rootPanel.removeAll() rootPanel.add(targetComponent, BorderLayout.CENTER) } } private fun canSwitchTo(newStandalone: Boolean): Boolean { if (newStandalone) { return plot != null && plot?.error == null } else { return plot == null || snapshot != null } } private fun showPlotError(error: RPlotError) { val message = RPlotUtil.getErrorDescription(error) if (snapshot != null) { graphicsPanel.showMessageWithLink(message, RBundle.message("plot.viewer.switch.to.builtin")) { isStandalone = false } } else { graphicsPanel.showMessage(message) } } private fun scheduleRescalingIfNecessary() { if (usesViewer || !hasGraphics) { return } if (isAutoResizeEnabled || localResolution != targetResolution || oldStandalone != isStandalone) { scheduleRescaling() } } private fun scheduleRescaling() { queue.queue(object : Update(RESIZE_TASK_IDENTITY) { override fun run() { rescaleIfNecessary() } }) } fun rescaleIfNecessary(preferredSize: Dimension? = null) { if (usesViewer) { return } val oldSize = graphicsPanel.imageSize val newSize = preferredSize ?: preferredImageSize.takeIf { isAutoResizeEnabled } ?: oldSize if (newSize != null && newSize.isValid) { if (oldSize != newSize || localResolution != targetResolution || oldStandalone != isStandalone) { // Note: there might be lots of attempts to resize image on IDE startup // but most of them will fail (and throw an exception) // due to the parent being disposed if (!Disposer.isDisposed(parent) && isVisible) { rescale(newSize, targetResolution) } } } } private fun rescaleIfStandalone() { // Note: check for `localResolution` prevents useless rescales on IDE startup // when the first rescale hasn't been completed yet if (isStandalone && !isAutoResizeEnabled && localResolution != null) { val newSize = graphicsPanel.imageSize if (newSize != null && newSize.isValid) { rescale(newSize, targetResolution) } } } private fun rescale(newSize: Dimension, newResolution: Int?) { if (isStandalone) { plot?.let { plot -> if (plot.error != null) { showPlotError(plot.error!!) } else { rescale(plot, newSize, newResolution) } } } else { snapshot?.let { snapshot -> rescale(snapshot, newSize, newResolution) } } } private fun rescale(plot: RPlot, newSize: Dimension, newResolution: Int?) { runAsync { val parameters = RGraphicsUtils.ScreenParameters(newSize, newResolution) val image = RPlotUtil.createImage(plot, parameters, manager.isDarkModeEnabled, isPreview = false) localResolution = newResolution oldStandalone = isStandalone graphicsPanel.showBufferedImage(image) scheduleRescalingIfNecessary() } } private fun rescale(snapshot: RSnapshot, newSize: Dimension, newResolution: Int?) { if (!manager.isBusy) { manager.rescaleImage(snapshot.file, newSize, newResolution) { imageFile -> localResolution = manager.getImageResolution(imageFile) this.snapshot = RSnapshot.from(imageFile) oldStandalone = isStandalone graphicsPanel.showImage(imageFile) scheduleRescalingIfNecessary() } } else { scheduleRescaling() // Out of luck: try again in 500 ms } } companion object { private const val RESIZE_TIME_SPAN = 500 private const val RESIZE_TASK_NAME = "Resize graphics" private const val RESIZE_TASK_IDENTITY = "Resizing graphics" private val Dimension.isValid: Boolean get() = width > 0 && height > 0 } }