Inline button for view as image

[pycharm] PY-79803 Debugger: add button

Merge-request: IJ-MR-160372
Merged-by: Ekaterina Itsenko <ekaterina.itsenko@jetbrains.com>
(cherry picked from commit e2900c27343967e61033554e12cca97a0cc78e61)

GitOrigin-RevId: 454f42494582b5e3b4cf3342145f2316d65e8dd0
This commit is contained in:
ekaterina.itsenko
2025-04-15 17:23:34 +00:00
committed by intellij-monorepo-bot
parent d889654caf
commit 3de80b5d6e
3 changed files with 105 additions and 10 deletions

View File

@@ -20,5 +20,7 @@
<orderEntry type="module" module-name="intellij.platform.ide.util.io" />
<orderEntry type="module" module-name="intellij.platform.debugger.impl" />
<orderEntry type="module" module-name="intellij.python.syntax.core" />
<orderEntry type="module" module-name="intellij.platform.core.ui" />
<orderEntry type="module" module-name="intellij.platform.ide.core.impl" />
</component>
</module>

View File

@@ -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

View File

@@ -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