GuiDebugDevToolUtility.java
package swingtree.components;
import org.jspecify.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import sprouts.From;
import sprouts.Tuple;
import sprouts.Var;
import sprouts.Viewable;
import swingtree.SwingTree;
import swingtree.UI;
import swingtree.input.Keyboard;
import swingtree.layout.Bounds;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
final class GuiDebugDevToolUtility {
private static final Logger log = LoggerFactory.getLogger(GuiDebugDevToolUtility.class);
private static final String SWING_TREE_DEV_TOOLS_SHORTCUT_ACTION_KEY = "SwingTreeDevToolsShortcut";
private static final UI.Color FOCUS_COLOR = UI.Color.ofRgb(0, 90, 200);
private static final UI.Color SELECTION_COLOR = UI.Color.ofRgb(0, 142, 83);
private static final javax.swing.Action toggleDevToolsShortcutAction = new AbstractAction() {
@Override public void actionPerformed(ActionEvent e) {
SwingTree.get().setDevToolEnabled(
!SwingTree.get().isDevToolEnabled()
);
}
};
private static @Nullable Component focusedDebugComponent = null;
private static @Nullable Component selectedDebugComponent = null;
private static @Nullable DebugInfoWindow debugInfoWindow = null;
private GuiDebugDevToolUtility() {} // A utility class can not be instantiated!
static void setupGlobalDevToolsShortcutFor(JRootPane rootPane) {
String keyStrokeString = SwingTree.get().getDevToolKeyStrokeShortcut();
if ( !keyStrokeString.isEmpty() ) {
try {
KeyStroke keyStroke = KeyStroke.getKeyStroke(keyStrokeString);
if (keyStroke == null) {
throw new IllegalArgumentException("Invalid key stroke string for dev tools shortcut: '" + keyStrokeString + "'");
}
rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(keyStroke, SWING_TREE_DEV_TOOLS_SHORTCUT_ACTION_KEY);
rootPane.getActionMap().put(SWING_TREE_DEV_TOOLS_SHORTCUT_ACTION_KEY, toggleDevToolsShortcutAction);
} catch (RuntimeException e) {
log.error("Error while setting up dev tools shortcut with key stroke '{}': {}", keyStrokeString, e.getMessage(), e);
}
}
}
static void teardownGlobalDevToolsShortcutFor(JRootPane rootPane) {
String keyStrokeString = SwingTree.get().getDevToolKeyStrokeShortcut();
if ( !keyStrokeString.isEmpty() ) {
try {
KeyStroke keyStroke = KeyStroke.getKeyStroke(keyStrokeString);
if (keyStroke == null) {
throw new IllegalArgumentException("Invalid key stroke string for dev tools shortcut: '" + keyStrokeString + "'");
}
rootPane.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).remove(keyStroke);
rootPane.getActionMap().remove(SWING_TREE_DEV_TOOLS_SHORTCUT_ACTION_KEY);
} catch (RuntimeException e) {
log.error("Error while tearing down dev tools shortcut with key stroke '{}': {}", keyStrokeString, e.getMessage(), e);
}
}
}
static void initializeDebugToolFor(JRootPane rootPane) {
if ( !SwingTree.get().isDevToolEnabled() ) {
return;
}
Component any = rootPane.getContentPane();
checkDebugging(any, rootPane);
summonInfoDialog(rootPane);
}
static void processMouseMovementForLiveDebugging(JGlassPane glassPane, JRootPane rootPane, MouseEvent e) {
if ( !SwingTree.get().isDevToolEnabled() ) {
return;
}
Component deepest = findComponentForLiveDebugging(glassPane, rootPane, e);
if ( checkDebugging(deepest, rootPane) ) {
rootPane.repaint();
}
}
private static java.awt.@Nullable Component findComponentForLiveDebugging(
JGlassPane glassPane,
JRootPane rootPane,
MouseEvent e
) {
Container content = rootPane.getContentPane();
// Convert glass pane coordinates → content pane coordinates
Point contentPoint = SwingUtilities.convertPoint(
glassPane,
e.getPoint(),
content
);
if (contentPoint.x < 0 || contentPoint.y < 0)
return null;
return SwingUtilities.getDeepestComponentAt(
content,
contentPoint.x,
contentPoint.y
);
}
private static boolean checkDebugging(java.awt.@Nullable Component deepest, JRootPane currentRootPane) {
if (deepest != null && GuiDebugDevToolUtility.focusedDebugComponent != deepest) {
if ( GuiDebugDevToolUtility.focusedDebugComponent != null ) {
JRootPane rootPaneOfFocused = SwingUtilities.getRootPane(GuiDebugDevToolUtility.focusedDebugComponent);
if ( rootPaneOfFocused != null && rootPaneOfFocused != currentRootPane ) {
// We are switching to a different root pane, so we need to repaint the old one to clear the debug overlay there!
rootPaneOfFocused.repaint();
}
}
GuiDebugDevToolUtility.focusedDebugComponent = deepest;
// The debug component is being highlighted similarly as in inspector mode on a browser...
if ( GuiDebugDevToolUtility.debugInfoWindow != null ) {
GuiDebugDevToolUtility.debugInfoWindow.debugState.set(new ComponentDebugInfo(deepest));
}
return true;
}
return false;
}
static void findAndSelectComponentForDebug(JRootPane rootPane, MouseEvent e) {
if ( !SwingTree.get().isDevToolEnabled() ) {
return;
}
if ( GuiDebugDevToolUtility.focusedDebugComponent == null ) {
return;
}
if (!Keyboard.get().isPressed(Keyboard.Key.CONTROL)) {
return;
}
e.consume();// No other things should react to this event, since we are opening the debug dialog!
summonInfoDialog(rootPane);
}
private static void summonInfoDialog(JRootPane rootPane) {
if ( GuiDebugDevToolUtility.focusedDebugComponent == null ) {
return;
}
if ( selectedDebugComponent != focusedDebugComponent )
rootPane.repaint(); // Repaint to clear previous selection highlight
if ( GuiDebugDevToolUtility.selectedDebugComponent != null ) {
JRootPane rootPaneOfSelected = SwingUtilities.getRootPane(GuiDebugDevToolUtility.selectedDebugComponent);
if ( rootPaneOfSelected != null && rootPaneOfSelected != rootPane ) {
// We are switching the selection to a different root pane,
// so we need to repaint the old one to clear the selection debug overlay there!
rootPaneOfSelected.repaint();
}
}
// Select the currently focused debug component:
selectedDebugComponent = focusedDebugComponent;
if ( GuiDebugDevToolUtility.debugInfoWindow == null ) {
DebugInfoWindow newWindow = new DebugInfoWindow(
Var.of(new ComponentDebugInfo(GuiDebugDevToolUtility.focusedDebugComponent)),
Var.of(new ComponentDebugInfo(GuiDebugDevToolUtility.selectedDebugComponent))
);
newWindow.pack();
try {
findGoodPlacementForDebugWindow(newWindow, rootPane);
} catch (Exception e) {
log.error("Failed to establish a sensible layout for the inspection debug tool!", e);
}
newWindow.setVisible(true);
newWindow.addWindowListener(new WindowAdapter() {
@Override
public void windowClosing(WindowEvent e) {
GuiDebugDevToolUtility.debugInfoWindow = null;
newWindow.dispose();
// Disable debug mode:
SwingTree.get().setDevToolEnabled(false);
}
@Override
public void windowOpened(WindowEvent e) {
Window rootWindow = SwingUtilities.getWindowAncestor(rootPane);
if ( rootWindow != null ) {
// When the window opens, we want to transfer focus back to the root panes window:
SwingUtilities.invokeLater(rootWindow::requestFocus);
}
}
});
GuiDebugDevToolUtility.debugInfoWindow = newWindow;
} else {
GuiDebugDevToolUtility.debugInfoWindow.selectedDebugState.set(new ComponentDebugInfo(GuiDebugDevToolUtility.selectedDebugComponent));
}
}
private static void findGoodPlacementForDebugWindow(DebugInfoWindow debugWindow, JRootPane rootPane) {
// First up, let's get an overview of the setup: Get all screen devices!
GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
GraphicsDevice[] screens = ge.getScreenDevices();
GraphicsDevice mainScreen = null;
// We try to get the window of the root pane, since we want to place the debug window relative to it:
Window rootWindow = SwingUtilities.getWindowAncestor(rootPane);
if (rootWindow == null) {
// We can not find the main screen based on the root pane's window, so we just pick the primary screen:
mainScreen = ge.getDefaultScreenDevice();
} else {
// We try to find the screen device that contains the root window:
for (GraphicsDevice screen : screens) {
Rectangle screenBounds = screen.getDefaultConfiguration().getBounds();
if (screenBounds.contains(rootWindow.getLocation())) {
mainScreen = screen;
break;
}
}
// If no screen contains the root window (e.g. between displays), fall back to the default screen:
if (mainScreen == null) {
mainScreen = ge.getDefaultScreenDevice();
}
}
if ( mainScreen != null ) {
// Get bounds where we want to place the debug window:
Rectangle bounds = mainScreen.getDefaultConfiguration().getBounds();
debugWindow.setBounds(
bounds.x, bounds.y,
Math.max(1, debugWindow.getWidth()),
Math.max(debugWindow.getHeight(), bounds.height)
);
if ( rootWindow != null ) {
// If the root window is in full screen mode, we make room for the debug window by resizing the root window:
boolean isFullScreen = rootWindow instanceof Frame && (((Frame)rootWindow).getExtendedState() & Frame.MAXIMIZED_BOTH) == Frame.MAXIMIZED_BOTH;
Rectangle rootBounds = rootWindow.getBounds();
int epsilon = UI.scale(325); // To account for task bars and such, we give it some tolerance, so that the debug window can still be placed even if the root window is not exactly in full screen mode.
if (
isFullScreen || (
Math.abs(rootBounds.width - bounds.width) <= epsilon &&
Math.abs(rootBounds.height - bounds.height) <= (epsilon * 2) &&
Math.abs(rootBounds.x - bounds.x) <= epsilon &&
Math.abs(rootBounds.y - bounds.y) <= (epsilon * 2)
)
) {
if ( isFullScreen ) // We need to set it to normal first, otherwise the bounds change might not work correctly!
((Frame)rootWindow).setExtendedState( Frame.NORMAL );
rootWindow.setBounds(
bounds.x + debugWindow.getWidth(),
rootBounds.y,
Math.max(1, rootBounds.width - debugWindow.getWidth()),
Math.max(1, rootBounds.height)
);
}
}
}
}
static void paintDebugOverlay(Graphics2D g2d, JGlassPane glassPane) {
if (!SwingTree.get().isDevToolEnabled() || focusedDebugComponent == null)
return;
if (!focusedDebugComponent.isShowing())
return;
// Setup font:
{
Font font = g2d.getFont();
if ( font != null ) {
g2d.setFont(
font.deriveFont(UI.scale(12f)) // size!
);
}
}
// Debug information should also be allowed to look good! :)
g2d.setRenderingHint( RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON );
if ( selectedDebugComponent != focusedDebugComponent && focusedDebugComponent.isShowing() ) {
tryRenderDebugOverlayFor(g2d, glassPane, focusedDebugComponent, FOCUS_COLOR);
}
if ( selectedDebugComponent != null && selectedDebugComponent.isShowing() ) {
tryRenderDebugOverlayFor(g2d, glassPane, selectedDebugComponent, SELECTION_COLOR);
}
}
private static void tryRenderDebugOverlayFor(
Graphics2D g2d,
JGlassPane glassPane,
java.awt.Component inspectedComponent,
Color themeColor
){
// We only render the overlay, if the inspected component has the same root pane as the glass pane:
JRootPane rootPaneOfInspected = SwingUtilities.getRootPane(inspectedComponent);
JRootPane rootPaneOfGlass = SwingUtilities.getRootPane(glassPane);
if ( rootPaneOfInspected != rootPaneOfGlass ) {
return;
}
Graphics2D overlayGraphics = null;
try {
overlayGraphics = (Graphics2D) g2d.create();
renderDebugOverlayFor(overlayGraphics, glassPane, inspectedComponent, themeColor);
} catch (RuntimeException e) {
String componentName = inspectedComponent.getName() != null ? inspectedComponent.getName() : "<unnamed>";
log.error(
"Error while rendering debug overlay for component '{}' ({})",
componentName,
inspectedComponent.getClass().getName(),
e
);
} finally {
if (overlayGraphics != null) {
overlayGraphics.dispose();
}
}
}
private static void renderDebugOverlayFor(
Graphics2D g2d,
JGlassPane glassPane,
java.awt.Component inspectedComponent,
Color themeColor
) {
Insets insets = new Insets(0,0,0,0);
if ( inspectedComponent instanceof JComponent ) {
insets = ((JComponent)inspectedComponent).getInsets(insets);
}
final int entireWidth = glassPane.getWidth();
final int entireHeight = glassPane.getHeight();
// Convert component bounds → glass pane coordinate space
final Rectangle bounds = SwingUtilities.convertRectangle(
inspectedComponent.getParent(),
inspectedComponent.getBounds(),
glassPane
);
final Rectangle contentBounds = new Rectangle(
bounds.x + insets.left,
bounds.y + insets.top,
bounds.width - insets.left - insets.right,
bounds.height - insets.top - insets.bottom
);
// Use alpha composite for transparency
Composite oldComposite = g2d.getComposite();
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.25f));
// Fill overlay
Color brighter = themeColor.brighter();
g2d.setColor(new Color(brighter.getRed(), brighter.getGreen(), brighter.getBlue(), 150));
g2d.fill(bounds);
// Restore full opacity for border
g2d.setComposite(oldComposite);
g2d.setXORMode(Color.WHITE); // To ensure readability over all GUIs
int strokeWidth = UI.scale(2);
g2d.setStroke(new BasicStroke(strokeWidth));
g2d.setColor(new Color(themeColor.getRed(), themeColor.getGreen(), themeColor.getBlue(), 200));
g2d.draw(bounds);
g2d.setStroke(new BasicStroke(UI.scale(1), BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 0, new float[]{6}, 0));
g2d.setColor(new Color(0, 0, 0, 200));
g2d.drawLine(
contentBounds.x, 0,
contentBounds.x, entireHeight
);
g2d.drawLine(
0, contentBounds.y,
entireWidth, contentBounds.y
);
g2d.drawLine(
contentBounds.x + contentBounds.width, 0,
contentBounds.x + contentBounds.width, entireHeight
);
g2d.drawLine(
0, contentBounds.y + contentBounds.height,
entireWidth, contentBounds.y + contentBounds.height
);
g2d.setPaintMode(); // Reset XOR mode!!
// Optional: component info label
String label = getClassNameWithoutPackage(inspectedComponent.getClass())
+ " " + bounds.width + "x" + bounds.height;
FontMetrics fm = g2d.getFontMetrics();
int textWidth = fm.stringWidth(label);
int textHeight = fm.getHeight();
int labelX = 1 + ((bounds.x+textWidth) >= entireWidth ? entireWidth-textWidth-UI.scale(6) : bounds.x);
int labelY = -1 + (bounds.y - textHeight >= 0 ? bounds.y - 2 : bounds.y + textHeight + 2);
int labelPadding = strokeWidth / 2;
Rectangle labelBanner = new Rectangle(
labelX - labelPadding,
labelY - textHeight - labelPadding,
textWidth + UI.scale(6) + labelPadding * 2,
textHeight + labelPadding * 2
);
g2d.setStroke(new BasicStroke(2));
g2d.setColor(lerp(themeColor, Color.BLACK, 0.5));
g2d.fillRoundRect(labelBanner.x+1, labelBanner.y+1, labelBanner.width, labelBanner.height, 9, 9);
g2d.setColor(lerp(themeColor, Color.WHITE, 0.5));
g2d.fillRoundRect(labelBanner.x-1, labelBanner.y-1, labelBanner.width, labelBanner.height, 9, 9);
g2d.setColor(themeColor);
g2d.fillRoundRect(labelBanner.x, labelBanner.y, labelBanner.width, labelBanner.height, 8, 8);
g2d.setColor(Color.WHITE);
g2d.drawString(label, labelX + 3, labelY - UI.scale(4));
}
private static Color lerp(Color a, Color b, double t) {
int r = (int) (a.getRed() + (b.getRed() - a.getRed()) * t);
int g = (int) (a.getGreen() + (b.getGreen() - a.getGreen()) * t);
int bCol = (int) (a.getBlue() + (b.getBlue() - a.getBlue()) * t);
int aCol = (int) (a.getAlpha() + (b.getAlpha() - a.getAlpha()) * t);
return new Color(r, g, bCol, aCol);
}
private static String stackTraceToString(Tuple<StackTraceElement> stackTraceElements) {
StringBuilder builder = new StringBuilder();
for (StackTraceElement traceElement : stackTraceElements)
builder = builder.append("\tat ").append(traceElement).append("\n");
return builder.toString();
}
private static String getClassNameWithoutPackage(Class<?> type) {
String name = type.getName(); // e.g. com.example.Outer$Inner
int lastDot = name.lastIndexOf('.');
String rawName = lastDot >= 0 ? name.substring(lastDot + 1) : name;
return rawName.replace("$", ".");
}
private static class DebugInfoWindow extends JFrame {
@SuppressWarnings({"FieldCanBeLocal", "unused"})
private final Viewable<Boolean> isDevToolsEnabled; // Important, we need to keep the reference to keep the binding alive! (otherwise it can get garbage collected...)
private final Var<ComponentDebugInfo> debugState;
private final Var<ComponentDebugInfo> selectedDebugState;
DebugInfoWindow(Var<ComponentDebugInfo> debugState, Var<ComponentDebugInfo> selectedDebugState) {
this.debugState = debugState;
this.selectedDebugState = selectedDebugState;
this.isDevToolsEnabled = SwingTree.get().isDevToolEnabledView().onChange(From.ALL, it -> {
if ( !it.currentValue().orElse(false) ) {
this.dispose();
GuiDebugDevToolUtility.debugInfoWindow = null;
}
});
setTitle(titleFromFocus(focusedDebugComponent));
Viewable.cast(debugState).onChange(From.ALL, it ->{
setTitle(titleFromFocus(focusedDebugComponent));
});
this.add(
UI.splitPane(UI.Align.HORIZONTAL)
.add(
buildInfoDisplay(debugState, FOCUS_COLOR,
"<html>" +
"This overview shows <b>the component that is currently set to be in focus</b> by the mouse cursor. <br>" +
"You can select this component <b>by holding CTRL and then clicking on it</b> in the application window." +
"</html>"
)
)
.add(
buildInfoDisplay(selectedDebugState, SELECTION_COLOR,
"<html>" +
"This is the currently selected component. <br>" +
"To select another component, <b>hold CTRL and click on it</b> in the application window." +
"</html>"
)
)
.get(JSplitPane.class)
);
}
private static String titleFromFocus(@Nullable Component component) {
if ( component == null ) {
return "Inspecting: <no frame in focus>";
}
// Traversing upward, we want to find the last component in the hierarchy which is not Swing/AWT or SwingTree!
// So we are looking for the first user based component in the hierarchy from the roots perspective...
// So we traverse the hierarchy from the selected component up to the root,
// and check if the class name of the component contains "javax.swing", "java.awt" or "swingtree".
Component probablyUserBasedComponent = null;
Component current = component;
Component root = component;
while (current != null) {
String className = current.getClass().getName();
if (
!className.startsWith("javax.swing") &&
!className.startsWith("java.awt") &&
!className.startsWith("swingtree")
) {
probablyUserBasedComponent = current;
}
current = current.getParent();
if (current != null) {
root = current;
}
}
if ( probablyUserBasedComponent != null ) {
return "Inspecting: " + getClassNameWithoutPackage(probablyUserBasedComponent.getClass());
}
// If we can not find any user based component, we just return the root component's class name:
return "Inspecting: " + getClassNameWithoutPackage(root.getClass());
}
private static JPanel buildInfoDisplay(Var<ComponentDebugInfo> debugState, UI.Color themeColor, String toolTip) {
return
UI.panel("fill, wrap 1").withPrefSize(650, 375)
.withStyle( it -> it
.gradient( g -> g
.type(UI.GradientType.RADIAL)
.offset(-100, 30)
.span(UI.Span.TOP_RIGHT_TO_BOTTOM_LEFT)
.colors(
themeColor.brighterBy(0.70).desaturateBy(0.8),
themeColor.brighterBy(0.50).desaturateBy(0.7),
themeColor
)
)
)
.add("growx, pushx",
UI.box("fill, gap rel 3", "[shrink][shrink][grow]")
.add("left",
UI.label(
debugState.viewAsString(it ->
getClassNameWithoutPackage(it.type())
)
)
.withTooltip(toolTip)
.withStyle( it -> it
.borderRadius(8)
.padding(4, 8, 4, 8)
.backgroundColor(themeColor)
.fontColor(UI.Color.WHITE)
.fontWeight(2)
)
)
.add("left",
UI.label(
debugState.viewAsString(it ->
(it.id().isEmpty() ? "" : "id='"+it.id()+"'")
)
)
)
.add("right",
UI.label(
debugState.viewAs(Bounds.class, ComponentDebugInfo::bounds)
.viewAsString(it ->
"x="+((int)it.location().x())+", " +
"y="+((int)it.location().y())+", " +
"width="+it.size().width().map(Number::intValue).orElse(-1)+", " +
"height="+it.size().height().map(Number::intValue).orElse(-1
)
))
)
)
.add("push, grow",
UI.tabbedPane()
.withStyle( it -> it
.borderRadiusAt(UI.Corner.TOP_LEFT, 24)
.borderRadiusAt(UI.Corner.TOP_RIGHT, 24)
)
.add(
UI.tab("Source Trace")
.add(
UI.scrollPane().add(
UI.textArea(
debugState.viewAsString(it -> it.type().getCanonicalName() +"\n"+ stackTraceToString(it.sourceCodeLocation()))
)
.isEditableIf(false)
)
)
)
.add(
UI.tab("Style")
.add(
UI.scrollPane().add(
UI.textArea(
debugState.viewAsString(it -> {
try {
return prettyRecord(it.styleConf().toString());
} catch (RuntimeException e) {
log.error("Error while pretty-printing style conf: {}", e.getMessage(), e);
return it.styleConf().toString();
}
})
)
.isEditableIf(false)
)
)
)
.add(
UI.tab("Layout")
.add(
UI.scrollPane().add(
UI.textArea(
debugState.viewAsString(ComponentDebugInfo::layoutInformation)
)
.isEditableIf(false)
)
)
)
.add(
UI.tab("toString")
.add(
UI.scrollPane().add(
UI.textArea(
debugState.viewAsString(it -> {
try {
return prettyRecord(it.asString());
} catch (RuntimeException e) {
log.error("Error while pretty-printing style conf: {}", e.getMessage(), e);
return it.asString();
}
})
)
.isEditableIf(false)
)
)
)
)
.get(JPanel.class);
}
}
private static String prettyRecord(String input) {
if (input.isEmpty())
return input;
StringBuilder out = new StringBuilder(input.length() + 128);
int depth = 0;
boolean newLine = false;
java.util.regex.Pattern colorPattern = java.util.regex.Pattern.compile("rgb[a]?\\([^)]*\\)");
for (int i = 0; i < input.length(); i++) {
try {
i = maybeSkip(i, input, "[]", out);
i = maybeSkip(i, input, "[NONE]", out);
i = maybeSkip(i, input, "[EMPTY]", out);
i = maybeSkipRegex(i, input, colorPattern, out);
} catch (RuntimeException e) {
log.error("Error while pretty-printing record: {}", e.getMessage(), e);
}
char c = input.charAt(i);
switch (c) {
case '[' : {
out.append(c);
depth++;
out.append('\n');
indent(out, depth);
newLine = true;
break;
}
case ']' : {
depth--;
out.append('\n');
indent(out, depth);
out.append(c);
newLine = false;
break;
}
case ',' : {
out.append(c);
out.append('\n');
indent(out, depth);
newLine = true;
break;
}
default : {
if (newLine && Character.isWhitespace(c)) {
// skip whitespace immediately after newline
break;
}
out.append(c);
newLine = false;
break;
}
}
}
return out.toString();
}
private static int maybeSkip(int i, String input, String toMatch, StringBuilder out) {
if ( (i+toMatch.length()) <= input.length() ) {
// Special handling for 'toMatch' to keep it compact
if ( input.startsWith(toMatch, i) ) {
out.append(toMatch);
return i + toMatch.length();
}
}
return i;
}
private static int maybeSkipRegex(int i, String input, java.util.regex.Pattern pattern, StringBuilder out) {
java.util.regex.Matcher matcher = pattern.matcher(input);
matcher.region(i, input.length());
if (matcher.lookingAt()) {
String match = matcher.group();
out.append(match);
return i + match.length();
}
return i;
}
private static void indent(StringBuilder sb, int depth) {
for (int i = 0; i < Math.max(0, depth); i++) {
sb.append(" ");
}
}
}