diff --git a/python/intellij.python.pydev.iml b/python/intellij.python.pydev.iml
index b95a3eb76eab..6ba9f5b7870e 100644
--- a/python/intellij.python.pydev.iml
+++ b/python/intellij.python.pydev.iml
@@ -20,5 +20,7 @@
+
+
\ No newline at end of file
diff --git a/python/pydevSrc/resources/messages/PydevBundle.properties b/python/pydevSrc/resources/messages/PydevBundle.properties
index 6fe1c7ba915b..ec4be95ae384 100644
--- a/python/pydevSrc/resources/messages/PydevBundle.properties
+++ b/python/pydevSrc/resources/messages/PydevBundle.properties
@@ -1,5 +1,6 @@
pydev.loading.value=... Loading Value
pydev.show.value=Show Value
pydev.view.as=...View as {0}
+pydev.view.as.image=...View as Image
pydev.value.protected.attributes.group.name=Protected Attributes
pydev.error.message.failed.to.find.free.socket.port=Failed to find a free socket port
\ No newline at end of file
diff --git a/python/pydevSrc/src/com/jetbrains/python/debugger/PyDebugValue.java b/python/pydevSrc/src/com/jetbrains/python/debugger/PyDebugValue.java
index f44ddd47f043..3c09d20cd177 100644
--- a/python/pydevSrc/src/com/jetbrains/python/debugger/PyDebugValue.java
+++ b/python/pydevSrc/src/com/jetbrains/python/debugger/PyDebugValue.java
@@ -5,6 +5,12 @@ import com.google.common.base.Strings;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.Maps;
import com.intellij.icons.AllIcons;
+import com.intellij.ide.DataManager;
+import com.intellij.openapi.actionSystem.ActionManager;
+import com.intellij.openapi.actionSystem.AnAction;
+import com.intellij.openapi.actionSystem.AnActionEvent;
+import com.intellij.openapi.actionSystem.DataContext;
+import com.intellij.openapi.util.registry.Registry;
import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.diagnostic.Logger;
@@ -20,9 +26,10 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.swing.*;
-import java.util.ArrayList;
+import java.awt.*;
+import java.util.*;
+import java.awt.event.MouseEvent;
import java.util.List;
-import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -353,7 +360,7 @@ public class PyDebugValue extends XNamedValue {
public void computePresentation(@NotNull XValueNode node, @NotNull XValuePlace place) {
String value = PyTypeHandler.format(this);
setFullValueEvaluator(node, value);
- setConfigureTypeRenderersLink(node);
+ setAdditionalLinks(node);
if (value.length() >= MAX_VALUE) {
value = value.substring(0, MAX_VALUE);
}
@@ -361,6 +368,25 @@ public class PyDebugValue extends XNamedValue {
setElementPresentation(node, value);
}
+ private void setAdditionalLinks(@NotNull XValueNode node) {
+ if (node instanceof XValueNodeImpl valueNode) {
+ if (checkAndEnableViewAsImageVisibility(this)) {
+ addViewAsImageLink(valueNode);
+ }
+ addConfigureTypeRendererLink(valueNode);
+ }
+ }
+
+ private void addConfigureTypeRendererLink(@NotNull XValueNodeImpl valueNode) {
+ String typeRendererId = getTypeRendererId();
+ if (typeRendererId != null) {
+ XDebuggerTreeNodeHyperlink link = myFrameAccessor.getUserTypeRenderersLink(typeRendererId);
+ if (link != null) {
+ valueNode.addAdditionalHyperlink(link);
+ }
+ }
+ }
+
public void updateNodeValueAfterLoading(@NotNull XValueNode node,
@NotNull String value,
@NotNull @Nls String linkText,
@@ -463,19 +489,85 @@ public class PyDebugValue extends XNamedValue {
}
return;
}
+
+ if (node instanceof XValueNodeImpl valueNode) {
+ addViewAsImageLink(valueNode);
+ }
String linkText = PydevBundle.message("pydev.view.as", postfix);
node.setFullValueEvaluator(new PyNumericContainerValueEvaluator(linkText, myFrameAccessor, treeName));
}
- private void setConfigureTypeRenderersLink(@NotNull XValueNode node) {
- String typeRendererId = getTypeRendererId();
- if (node instanceof XValueNodeImpl valueNode) {
- valueNode.clearAdditionalHyperlinks();
- if (typeRendererId != null) {
- XDebuggerTreeNodeHyperlink link = myFrameAccessor.getUserTypeRenderersLink(typeRendererId);
- if (link != null) valueNode.addAdditionalHyperlink(link);
+ private static void addViewAsImageLink(XValueNodeImpl valueNode) {
+ if (!checkAndShowViewAsImageOnScreen((PyDebugValue)valueNode.getXValue()))
+ return;
+ String viewAsImageText = PydevBundle.message("pydev.view.as.image");
+ valueNode.addAdditionalHyperlink(new XDebuggerTreeNodeHyperlink(viewAsImageText) {
+ @Override
+ public void onClick(MouseEvent event) {
+ AnAction action = ActionManager.getInstance().getAction("JupyterShowAsImageAction");
+ DataContext dataContext = DataManager.getInstance().getDataContext((Component)event.getSource());
+ AnActionEvent actionEvent = AnActionEvent.createFromAnAction(
+ action,
+ null,
+ "JupyterShowAsImageAction",
+ dataContext
+ );
+ action.actionPerformed(actionEvent);
}
+
+ @Override
+ public boolean alwaysOnScreen() {
+ return true;
+ }
+ });
+ }
+
+ private static boolean checkAndShowViewAsImageOnScreen(PyDebugValue debugValue) {
+ boolean showViewAsImage = Registry.get("actions.show.as.image.visibility").asBoolean();
+ if (!showViewAsImage) {
+ return false;
}
+ return checkAndEnableViewAsImageVisibility(debugValue);
+ }
+
+ private static boolean checkAndEnableViewAsImageVisibility(PyDebugValue debugValue) {
+ String nodeType = debugValue.getType();
+ return switch (Objects.requireNonNull(nodeType)) {
+ case NodeTypes.NDARRAY_NODE_TYPE, NodeTypes.EAGER_TENSOR_NODE_TYPE, NodeTypes.RESOURCE_VARIABLE_NODE_TYPE,
+ NodeTypes.SPARSE_TENSOR_NODE_TYPE, NodeTypes.TENSOR_NODE_TYPE -> {
+ int[] shape = extractShape(debugValue);
+ yield isValidArrayShape(shape);
+ }
+ case NodeTypes.IMAGE_NODE_TYPE, NodeTypes.FIGURE_NODE_TYPE -> true;
+ default -> false;
+ };
+ }
+
+ private static int[] extractShape(PyDebugValue debugValue) {
+ String shapeString = debugValue.getShape() == null ? "" : debugValue.getShape();
+ return Arrays.stream(shapeString.replace("(", "").replace(")", "").split(","))
+ .map(String::trim)
+ .mapToInt(s -> {
+ try {
+ return Integer.parseInt(s);
+ }
+ catch (NumberFormatException e) {
+ return Integer.MIN_VALUE;
+ }
+ })
+ .filter(value -> value != Integer.MIN_VALUE)
+ .toArray();
+ }
+
+ private static boolean isValidArrayShape(int[] shape) {
+ if (shape == null || shape.length == 0) {
+ return false;
+ }
+ return switch (shape.length) {
+ case 1, 2 -> true;
+ case 3 -> shape[2] == 3 || shape[2] == 4 || shape[2] == 1;
+ default -> false;
+ };
}
@Override