diff --git a/app/src/processing/app/ui/Editor.java b/app/src/processing/app/ui/Editor.java index 0437240b3..c29748612 100644 --- a/app/src/processing/app/ui/Editor.java +++ b/app/src/processing/app/ui/Editor.java @@ -2945,4 +2945,13 @@ public void show(Component component, int x, int y) { super.show(component, x, y); } } + + /** + * Called when clicking on the version number in the footer. + * Return a string with diagnostic info from the sketch, + * or empty string (or null) if not implemented/available. + */ + public String getSketchDiagnostics() { + return ""; + } } diff --git a/app/src/processing/app/ui/EditorFooter.java b/app/src/processing/app/ui/EditorFooter.java index 94860a0ab..7efef4132 100644 --- a/app/src/processing/app/ui/EditorFooter.java +++ b/app/src/processing/app/ui/EditorFooter.java @@ -109,7 +109,7 @@ public void mousePressed(MouseEvent e) { Base.DEBUG = !Base.DEBUG; editor.updateDevelopMenu(); } - copyDebugInformationToClipboard(); + copyFullDiagnosticsToClipboard(); } }); @@ -120,13 +120,23 @@ public void mousePressed(MouseEvent e) { updateTheme(); } - public static void copyDebugInformationToClipboard() { - var debugInformation = String.join("\n", + public static String getSystemDebugInformation() { + return String.join("\n", "Version: " + Base.getVersionName(), "Revision: " + Base.getRevision(), "OS: " + System.getProperty("os.name") + " " + System.getProperty("os.version") + " " + System.getProperty("os.arch"), "Java: " + System.getProperty("java.version") + " " + System.getProperty("java.vendor") ); + } + + public static void copyDebugInformationToClipboard() { + var stringSelection = new StringSelection(getSystemDebugInformation()); + var clipboard = java.awt.Toolkit.getDefaultToolkit().getSystemClipboard(); + clipboard.setContents(stringSelection, null); + } + + public void copyFullDiagnosticsToClipboard() { + var debugInformation = getSystemDebugInformation() + "\n\n" + editor.getSketchDiagnostics(); var stringSelection = new StringSelection(debugInformation); var clipboard = java.awt.Toolkit.getDefaultToolkit().getSystemClipboard(); clipboard.setContents(stringSelection, null); diff --git a/java/src/processing/mode/java/JavaEditor.java b/java/src/processing/mode/java/JavaEditor.java index 8d9b2252c..561e38ba5 100644 --- a/java/src/processing/mode/java/JavaEditor.java +++ b/java/src/processing/mode/java/JavaEditor.java @@ -2234,4 +2234,14 @@ static private int howManyFloats(List> handles) { } return count; } + + @Override + public String getSketchDiagnostics() { + if (debugger.isStarted()) { + return debugger.getDiagnostics(); + } else if (runtime != null) { + return Debugger.getDiagnostics(runtime); + } + return super.getSketchDiagnostics(); + } } diff --git a/java/src/processing/mode/java/debug/Debugger.java b/java/src/processing/mode/java/debug/Debugger.java index 013679320..e9d42895b 100644 --- a/java/src/processing/mode/java/debug/Debugger.java +++ b/java/src/processing/mode/java/debug/Debugger.java @@ -31,6 +31,7 @@ import java.awt.event.KeyEvent; import java.io.*; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -458,6 +459,124 @@ public synchronized void stepOut() { } + + /** + * Get diagnostics from the sketch, whether paused or running. + * If running, it will temporarily suspend the VM. + */ + public String getDiagnostics() { + return getDiagnostics(runtime); + } + + + /** + * Static helper to fetch diagnostics from a Runner, even if not debugging. + * Uses field reads instead of method invocations to avoid thread state issues. + */ + public static String getDiagnostics(Runner targetRuntime) { + if (targetRuntime == null) return ""; + VirtualMachine targetVM = targetRuntime.vm(); + if (targetVM == null) return ""; + + targetVM.suspend(); + try { + // Find the PApplet subclass + List pAppletClasses = targetVM.classesByName("processing.core.PApplet"); + if (pAppletClasses.isEmpty()) { + return "processing.core.PApplet not found in VM"; + } + ClassType pAppletBase = (ClassType) pAppletClasses.get(0); + + ClassType sketchClass = null; + for (ReferenceType type : targetVM.allClasses()) { + if (type instanceof ClassType) { + ClassType ct = (ClassType) type; + ClassType superclass = ct.superclass(); + while (superclass != null) { + if (superclass.equals(pAppletBase)) { + sketchClass = ct; + break; + } + superclass = superclass.superclass(); + } + if (sketchClass != null) break; + } + } + + if (sketchClass == null) { + return "Could not find sketch class extending PApplet"; + } + + // Find instance + List instances = sketchClass.instances(1); + if (instances.isEmpty()) { + return "No instance of " + sketchClass.name() + " found"; + } + ObjectReference appletInstance = instances.get(0); + + // Build diagnostics by reading fields directly (no thread required) + StringBuilder diag = new StringBuilder(); + diag.append("Sketch Diagnostics:\n"); + diag.append(" Class: ").append(sketchClass.name()).append("\n"); + + // Read PApplet fields + appendField(diag, appletInstance, pAppletBase, "width"); + appendField(diag, appletInstance, pAppletBase, "height"); + appendField(diag, appletInstance, pAppletBase, "pixelDensity"); + appendField(diag, appletInstance, pAppletBase, "frameCount"); + appendField(diag, appletInstance, pAppletBase, "frameRate"); + appendField(diag, appletInstance, pAppletBase, "focused"); + + // Try to get renderer class name from 'g' field (PGraphics) + try { + Field gField = pAppletBase.fieldByName("g"); + if (gField != null) { + Value gValue = appletInstance.getValue(gField); + if (gValue instanceof ObjectReference) { + ObjectReference graphics = (ObjectReference) gValue; + diag.append(" renderer: ").append(graphics.referenceType().name()).append("\n"); + } + } + } catch (Exception e) { + diag.append(" renderer: (unavailable)\n"); + } + + return diag.toString(); + + } catch (Exception e) { + return "Error gathering diagnostics: " + e.toString(); + } finally { + targetVM.resume(); + } + } + + /** + * Helper to append a field value to the diagnostics string. + */ + private static void appendField(StringBuilder sb, ObjectReference obj, ClassType type, String fieldName) { + try { + Field field = type.fieldByName(fieldName); + if (field != null) { + Value value = obj.getValue(field); + sb.append(" ").append(fieldName).append(": "); + if (value == null) { + sb.append("null"); + } else if (value instanceof com.sun.jdi.PrimitiveValue) { + sb.append(value.toString()); + } else if (value instanceof StringReference) { + sb.append(((StringReference) value).value()); + } else { + sb.append(value.toString()); + } + sb.append("\n"); + } + } catch (Exception e) { + sb.append(" ").append(fieldName).append(": (error: ").append(e.getMessage()).append(")\n"); + } + } + + + // /** Print the current stack trace. */ // public synchronized void printStackTrace() { // if (isStarted()) {