Fixed Decompiler message area not going away when changing functions

This commit is contained in:
dragonmacher
2026-06-18 22:39:45 -04:00
parent 4a802292a1
commit 2517de24d5
6 changed files with 101 additions and 42 deletions

View File

@@ -14,6 +14,9 @@ color.fg.decompiler.global = color.palette.darkcyan
color.fg.decompiler.special = color.palette.crimson color.fg.decompiler.special = color.palette.crimson
color.fg.decompiler.external.function = color.palette.fuchsia color.fg.decompiler.external.function = color.palette.fuchsia
color.fg.decompiler.message = color.palette.black
color.bg.decompiler.message = color.bg.visualgraph.message
color.bg.decompiler.current.variable = color.palette.highlight.transparent.yellow color.bg.decompiler.current.variable = color.palette.highlight.transparent.yellow
color.bg.decompiler.highlights.middle.mouse = color.bg.highlight color.bg.decompiler.highlights.middle.mouse = color.bg.highlight
@@ -46,8 +49,11 @@ icon.decompiler.action.display.lock = lock.gif
font.decompiler = font.monospaced font.decompiler = font.monospaced
font.decompiler.pcode.dfg = font.graphdisplay.default font.decompiler.pcode.dfg = font.graphdisplay.default
font.decompiler.message = font.graph.component.message
[Dark Defaults] [Dark Defaults]
# the message text needs to be dark in dark mode
color.fg.decompiler.message = rgb(0, 0, 0)
color.bg.decompiler.highlights.find = color.palette.cornflowerblue color.bg.decompiler.highlights.find = color.palette.cornflowerblue

View File

@@ -51,8 +51,7 @@ public class DecompilerController {
private int cacheSize; private int cacheSize;
public DecompilerController(ServiceProvider serviceProvider, DecompilerCallbackHandler handler, public DecompilerController(ServiceProvider serviceProvider, DecompilerCallbackHandler handler,
DecompileOptions options, DecompileOptions options, DecompilerClipboardProvider clipboard) {
DecompilerClipboardProvider clipboard) {
this.serviceProvider = serviceProvider; this.serviceProvider = serviceProvider;
this.cacheSize = options.getCacheSize(); this.cacheSize = options.getCacheSize();
this.callbackHandler = handler; this.callbackHandler = handler;
@@ -370,6 +369,13 @@ public class DecompilerController {
decompilerCache.invalidateAll(); decompilerCache.invalidateAll();
} }
public void clearCacheForCurrentFunction() {
Function function = getFunction();
if (function != null) {
decompilerCache.invalidate(function);
}
}
public void programClosed(Program closedProgram) { public void programClosed(Program closedProgram) {
for (Function function : decompilerCache.asMap().keySet()) { for (Function function : decompilerCache.asMap().keySet()) {
Program functionProgram = function.getProgram(); Program functionProgram = function.getProgram();

View File

@@ -1393,16 +1393,30 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field
* decompiler content panel and the line numbers panel} * decompiler content panel and the line numbers panel}
*/ */
public Rectangle getViewContentBounds() { public Rectangle getViewContentBounds() {
Insets viewInsets = scroller.getViewInsets();
int x = 0; // don't use insets here, since we want the full size including the line # panel
int y = viewInsets.top;
// This compensates for the optional parent border. We have guilty knowledge that our parent
// will have a non-empty border when in a snapshot
JComponent decorationPanel = (JComponent) getParent();
Insets parentInsets = decorationPanel.getInsets();
x += parentInsets.left;
y += parentInsets.top;
// The bounds we want includes both the extent size of the main decompiler view + the // The bounds we want includes both the extent size of the main decompiler view + the
// area that displays the line numbers which is not inside the IndexedScrollPane. The width // area that displays the line numbers which is not inside the IndexedScrollPane. The width
// of the line numbers panel can be found by looking at the x position of the scroller as // of the line numbers panel can be found by looking at the x position of the scroll view,
// it is offset by the line number panel's width. We are also assuming there are no borders // as it is offset by the line number panel's width.
// internal to the DecompilerPanel. If that changes, we would also need to factor in the
// insets.
Rectangle bounds = scroller.getBounds(); Rectangle bounds = scroller.getBounds();
Dimension scrollerSize = scroller.getViewExtentSize();
int lineNumberWidth = bounds.x; int lineNumberWidth = bounds.x;
return new Rectangle(0, 0, scrollerSize.width + lineNumberWidth, scrollerSize.height); int gap = viewInsets.left;
Dimension extent = scroller.getViewExtentSize();
int width = lineNumberWidth + gap + extent.width;
int height = extent.height;
return new Rectangle(x, y, width, height);
} }
private void buildPanels() { private void buildPanels() {

View File

@@ -86,6 +86,7 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter
private ToggleDockingAction displayUnreachableCodeToggle; private ToggleDockingAction displayUnreachableCodeToggle;
private ToggleDockingAction respectReadOnlyFlags; private ToggleDockingAction respectReadOnlyFlags;
private ToggleDockingAction toggleLockAction;
private final DecompilePlugin plugin; private final DecompilePlugin plugin;
private ClipboardService clipboardService; private ClipboardService clipboardService;
@@ -146,7 +147,7 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter
new DecompilerController(getTool(), this, decompilerOptions, clipboardProvider); new DecompilerController(getTool(), this, decompilerOptions, clipboardProvider);
DecompilerPanel decompilerPanel = controller.getDecompilerPanel(); DecompilerPanel decompilerPanel = controller.getDecompilerPanel();
// TODO move the hl controller into the panel // FUTURE move the hl controller into the panel
highlightController = new LocationClangHighlightController(); highlightController = new LocationClangHighlightController();
decompilerPanel.setHighlightController(highlightController); decompilerPanel.setHighlightController(highlightController);
decorationPanel = new DecoratorPanel(decompilerPanel, isConnected) { decorationPanel = new DecoratorPanel(decompilerPanel, isConnected) {
@@ -155,7 +156,6 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter
super.paint(g); super.paint(g);
overlayPainter.paintOverlay(g, decompilerPanel.getViewContentBounds()); overlayPainter.paintOverlay(g, decompilerPanel.getViewContentBounds());
} }
}; };
if (!isConnected) { if (!isConnected) {
@@ -326,6 +326,7 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter
if (!isVisible()) { if (!isVisible()) {
return; return;
} }
ToolOptions fieldOptions = tool.getOptions(GhidraOptions.CATEGORY_BROWSER_FIELDS); ToolOptions fieldOptions = tool.getOptions(GhidraOptions.CATEGORY_BROWSER_FIELDS);
ToolOptions opt = tool.getOptions(DecompilePlugin.OPTIONS_TITLE); ToolOptions opt = tool.getOptions(DecompilePlugin.OPTIONS_TITLE);
@@ -350,11 +351,15 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter
if (currentLocation != null) { if (currentLocation != null) {
if (lockDisplay) { if (lockDisplay) {
// Clear the cached results so the next time we come back to this function, it will
// get re-decompiled. If we don't do this, then we would need the ability to mark
// the cached results as needing update, so future loads of that cached function
// will trigger the refresh message.
controller.clearCacheForCurrentFunction();
setOverlayMessage(getOverlayRefreshMessage()); setOverlayMessage(getOverlayRefreshMessage());
} }
else { else {
controller.refreshDisplay(program, currentLocation, null); controller.refreshDisplay(program, currentLocation, null);
setOverlayMessage("");
} }
} }
} }
@@ -379,6 +384,11 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter
decorationPanel.repaint(); decorationPanel.repaint();
} }
private void clearOverlayMessage() {
overlayPainter.setMessage("");
decorationPanel.repaint();
}
private void refreshToggleButtons() { private void refreshToggleButtons() {
displayUnreachableCodeToggle.setSelected(!decompilerOptions.isEliminateUnreachable()); displayUnreachableCodeToggle.setSelected(!decompilerOptions.isEliminateUnreachable());
respectReadOnlyFlags.setSelected(!decompilerOptions.isRespectReadOnly()); respectReadOnlyFlags.setSelected(!decompilerOptions.isRespectReadOnly());
@@ -533,7 +543,6 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter
*/ */
void refresh() { void refresh() {
controller.refreshDisplay(program, currentLocation, null); controller.refreshDisplay(program, currentLocation, null);
setOverlayMessage("");
} }
/** /**
@@ -616,6 +625,8 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter
updateTitle(); updateTitle();
contextChanged(); contextChanged();
controller.setSelection(currentSelection); controller.setSelection(currentSelection);
clearOverlayMessage();
} }
@Override @Override
@@ -764,22 +775,34 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter
// invoke later to give the window manage a chance to create the new window // invoke later to give the window manage a chance to create the new window
// (its done in an invoke later) // (its done in an invoke later)
Swing.runLater(() -> { Swing.runLater(() -> {
initializeClone(newProvider);
});
}
ViewerPosition myViewPosition = controller.getDecompilerPanel().getViewerPosition(); private void initializeClone(DecompilerProvider newProvider) {
newProvider.doSetProgram(program); ViewerPosition myViewPosition = controller.getDecompilerPanel().getViewerPosition();
newProvider.doSetProgram(program);
// initialize the new provider's cache and then set the location // initialize the new provider's cache and then set the location
DecompileData myDecompileData = controller.getDecompileData(); DecompileData myDecompileData = controller.getDecompileData();
newProvider.controller.addToCache(myDecompileData); newProvider.controller.addToCache(myDecompileData);
newProvider.setLocation(currentLocation, myViewPosition); newProvider.setLocation(currentLocation, myViewPosition);
// transfer any state after the new decompiler is initialized // update the lock action and overlay message for the new provider
DecompilerPanel myPanel = getDecompilerPanel(); if (lockDisplay) {
DecompilerPanel newPanel = newProvider.getDecompilerPanel(); newProvider.toggleLockAction.setSelected(true);
newProvider.doWhenNotBusy(() -> { newProvider.lockDisplay = true;
newPanel.setViewerPosition(myViewPosition); if (overlayPainter.isActive()) {
newPanel.cloneHighlights(myPanel); newProvider.setOverlayMessage(getOverlayRefreshMessage());
}); }
}
// transfer any state after the new decompiler is initialized
DecompilerPanel myPanel = getDecompilerPanel();
DecompilerPanel newPanel = newProvider.getDecompilerPanel();
newProvider.doWhenNotBusy(() -> {
newPanel.setViewerPosition(myViewPosition);
newPanel.cloneHighlights(myPanel);
}); });
} }
@@ -835,7 +858,7 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter
private void createActions(boolean isConnected) { private void createActions(boolean isConnected) {
String owner = plugin.getName(); String owner = plugin.getName();
new ToggleActionBuilder("Lock Display", owner) toggleLockAction = new ToggleActionBuilder("Lock Display", owner)
.toolBarIcon(LOCK_DISPLAY_ICON) .toolBarIcon(LOCK_DISPLAY_ICON)
.description("Lock display for auto-updates, only update on manual refresh") .description("Lock display for auto-updates, only update on manual refresh")
.helpLocation(new HelpLocation(HelpTopics.DECOMPILER, "LockDisplay")) .helpLocation(new HelpLocation(HelpTopics.DECOMPILER, "LockDisplay"))

View File

@@ -20,7 +20,7 @@ import java.awt.*;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import generic.theme.GColor; import generic.theme.GColor;
import generic.theme.GThemeDefaults.Colors.Palette; import generic.theme.GThemeDefaults.Colors;
import generic.theme.Gui; import generic.theme.Gui;
/** /**
@@ -28,9 +28,13 @@ import generic.theme.Gui;
* needs to be refreshed manually. * needs to be refreshed manually.
*/ */
class OverlayMessagePainter { class OverlayMessagePainter {
private static final String FONT_MESSAGE_ID = "font.decompiler.message";
private static final Color COLOR_BG_GRADIENT = new GColor("color.bg.decompiler.message");
private static final Color COLOR_FG_MESSAGE = new GColor("color.fg.decompiler.message");
private static final int MARGIN = 10; private static final int MARGIN = 10;
private static final String FONT_ID = "font.graph.component.message";
private final Color gradientColor = new GColor("color.bg.visualgraph.message");
private String message; private String message;
void setMessage(String message) { void setMessage(String message) {
@@ -47,32 +51,35 @@ class OverlayMessagePainter {
} }
Graphics2D g2 = (Graphics2D) g; Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// this composite softens the text and color of the message // this composite softens the text and color of the message
Composite originalComposite = g2.getComposite(); Composite originalComposite = g2.getComposite();
g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SrcOver.getRule(), .60f)); g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SrcOver.getRule(), .60f));
// set up font // set up font
Font font = Gui.getFont(FONT_ID); Font font = Gui.getFont(FONT_MESSAGE_ID);
g.setFont(font); g.setFont(font);
Rectangle textBounds = font.getStringBounds(message, g2.getFontRenderContext()).getBounds(); Rectangle textBounds = font.getStringBounds(message, g2.getFontRenderContext()).getBounds();
int gx = bounds.x;
int gh = textBounds.height * 3; int gh = textBounds.height * 3;
int gy = bounds.height - gh; int gy = (bounds.y + bounds.height) - gh;
paintGradient(g2, 0, gy, bounds.width, gh); int gw = bounds.width;
paintGradient(g2, gx, gy, gw, gh);
// paint message // paint message
g2.setPaint(Palette.BLACK); g2.setPaint(COLOR_FG_MESSAGE);
int textX = bounds.width - textBounds.width - MARGIN; int textX = bounds.width - textBounds.width - MARGIN;
int textY = bounds.height - textBounds.height / 2; //text at bottom; account for baseline int textY = bounds.height - textBounds.height / 2; // text at bottom; account for baseline
g2.drawString(message, textX, textY); g2.drawString(message, textX, textY);
g2.setComposite(originalComposite); g2.setComposite(originalComposite);
} }
private void paintGradient(Graphics2D g2, int x, int y, int w, int h) { private void paintGradient(Graphics2D g2, int x, int y, int w, int h) {
Color[] colors = new Color[] { Color.WHITE, gradientColor }; Color[] colors = new Color[] { Colors.BACKGROUND, COLOR_BG_GRADIENT };
float[] fractions = new float[] { 0.0f, .95f }; float[] fractions = new float[] { 0.0f, .85f };
LinearGradientPaint gradiantPaint = LinearGradientPaint gradiantPaint =
new LinearGradientPaint(new Point(x, y), new Point(x, y + h), fractions, colors); new LinearGradientPaint(new Point(x, y), new Point(x, y + h), fractions, colors);
g2.setPaint(gradiantPaint); g2.setPaint(gradiantPaint);

View File

@@ -115,12 +115,15 @@ public class IndexedScrollPane extends JPanel implements IndexScrollListener {
} }
public Dimension getViewSize() { public Dimension getViewExtentSize() {
return new Dimension(comp.getPreferredSize().width, indexMapper.getViewHeight()); Dimension size = viewport.getExtentSize();
int w = size.width;
int h = size.height;
return new Dimension(w, h);
} }
public Dimension getViewExtentSize() { public Insets getViewInsets() {
return viewport.getExtentSize(); return scrollPane.getInsets();
} }
public void viewportStateChanged() { public void viewportStateChanged() {
@@ -311,7 +314,7 @@ public class IndexedScrollPane extends JPanel implements IndexScrollListener {
public void setColumnHeaderComp(JComponent comp) { public void setColumnHeaderComp(JComponent comp) {
scrollPane.setColumnHeaderView(comp); scrollPane.setColumnHeaderView(comp);
// SWING WORK AROUND - setting the header panel on a scrollpane that is horizontally // SWING WORK AROUND - setting the header panel on a scroll pane that is horizontally
// scrolled does not initially scroll the header to match the main view. Setting the // scrolled does not initially scroll the header to match the main view. Setting the
// horizontal position to 0 and back to where it was, resynchronizes the header with the // horizontal position to 0 and back to where it was, resynchronizes the header with the
// view. // view.