diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/listingpanel/ProgramBigListingModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/listingpanel/ProgramBigListingModel.java index 0862503c01..4ac0decc9a 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/listingpanel/ProgramBigListingModel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/viewer/listingpanel/ProgramBigListingModel.java @@ -36,6 +36,7 @@ import ghidra.program.model.address.*; import ghidra.program.model.data.*; import ghidra.program.model.listing.*; import ghidra.program.model.symbol.Reference; +import ghidra.util.datastruct.LRUMap; import ghidra.util.task.TaskMonitor; public class ProgramBigListingModel implements ListingModel, FormatModelListener, @@ -51,6 +52,9 @@ public class ProgramBigListingModel implements ListingModel, FormatModelListener private DummyFieldFactory dummyFactory; private List listeners = new ArrayList<>(); + // Use a cache so that simple arrowing to-and-fro with the keyboard will respond quickly + private LRUMap layoutCache = new LRUMap<>(10); + public ProgramBigListingModel(Program program, FormatManager formatMgr) { this.program = program; this.listing = program.getListing(); @@ -87,6 +91,10 @@ public class ProgramBigListingModel implements ListingModel, FormatModelListener showNonExternalFunctionPointerFormat = (Boolean) newValue; formatModelChanged(null); } + + // There are quite a few options that affect the display of the the layouts. Flush + // the cache on any change, as it is simpler than tracking individual options. + layoutCache.clear(); } @Override @@ -115,6 +123,16 @@ public class ProgramBigListingModel implements ListingModel, FormatModelListener @Override public Layout getLayout(Address addr, boolean isGapAddress) { + + Layout layout = layoutCache.get(addr); + if (layout == null) { + layout = doGetLayout(addr, isGapAddress); + layoutCache.put(addr, layout); + } + return layout; + } + + public Layout doGetLayout(Address addr, boolean isGapAddress) { List list = new ArrayList<>(); FieldFormatModel format; CodeUnit cu = listing.getCodeUnitAt(addr); @@ -477,12 +495,16 @@ public class ProgramBigListingModel implements ListingModel, FormatModelListener } protected void notifyDataChanged(boolean updateImmediately) { + layoutCache.clear(); + for (ListingModelListener listener : listeners) { listener.dataChanged(updateImmediately); } } private void notifyModelSizeChanged() { + layoutCache.clear(); + for (ListingModelListener listener : listeners) { listener.modelSizeChanged(); } @@ -523,15 +545,14 @@ public class ProgramBigListingModel implements ListingModel, FormatModelListener return program.isClosed(); } - /** - * @see ghidra.framework.model.DomainObjectListener#domainObjectChanged(ghidra.framework.model.DomainObjectChangedEvent) - */ @Override public void domainObjectChanged(DomainObjectChangedEvent ev) { - if (!program.isClosed()) { - boolean updateImmediately = ev.numRecords() <= 5; - notifyDataChanged(updateImmediately); + if (program.isClosed()) { + return; } + + boolean updateImmediately = ev.numRecords() <= 5; + notifyDataChanged(updateImmediately); } @Override diff --git a/Ghidra/Features/Base/src/main/java/ghidra/test/AbstractProgramBasedTest.java b/Ghidra/Features/Base/src/main/java/ghidra/test/AbstractProgramBasedTest.java index 790bbacdbe..e7bd073dd3 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/test/AbstractProgramBasedTest.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/test/AbstractProgramBasedTest.java @@ -70,7 +70,7 @@ public abstract class AbstractProgramBasedTest extends AbstractGhidraHeadedInteg * Override this method if you need to build your own program. * * @return the program to use for this test. - * @throws Exception if an exceptioun is thrown opening the program + * @throws Exception if an exception is thrown opening the program */ protected Program getProgram() throws Exception { return env.getProgram(getProgramName()); diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/OperandFieldFactoryTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/OperandFieldFactoryTest.java index 5aa18e7ac0..8d3eee5bd4 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/OperandFieldFactoryTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/OperandFieldFactoryTest.java @@ -56,10 +56,6 @@ public class OperandFieldFactoryTest extends AbstractGhidraHeadedIntegrationTest private Options fieldOptions; private Program program; - public OperandFieldFactoryTest() { - super(); - } - @Before public void setUp() throws Exception { diff --git a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/PostCommentFieldFactoryTest.java b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/PostCommentFieldFactoryTest.java index d888d05986..95cfdb90bd 100644 --- a/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/PostCommentFieldFactoryTest.java +++ b/Ghidra/Features/Base/src/test.slow/java/ghidra/app/util/viewer/field/PostCommentFieldFactoryTest.java @@ -41,10 +41,6 @@ public class PostCommentFieldFactoryTest extends AbstractGhidraHeadedIntegration private Options fieldOptions; private Program program; - public PostCommentFieldFactoryTest() { - super(); - } - @Before public void setUp() throws Exception { diff --git a/Ghidra/Features/Decompiler/src/main/help/help/topics/DecompilePlugin/Decompiler.htm b/Ghidra/Features/Decompiler/src/main/help/help/topics/DecompilePlugin/Decompiler.htm index 2501481b11..9c8d5c8509 100644 --- a/Ghidra/Features/Decompiler/src/main/help/help/topics/DecompilePlugin/Decompiler.htm +++ b/Ghidra/Features/Decompiler/src/main/help/help/topics/DecompilePlugin/Decompiler.htm @@ -516,14 +516,21 @@
  • Control-shift-click - Triggers the Listing in a Snapshots view to navigate to the address denoted by the symbol that was clicked.
  • + +
  • Middle-mouse - If you press the middle mouse + button the decompiler will highlight every occurrence of a variable or constant + under the current cursor location (the button changed in the tool options under + Browser Field->Cursor Text Highlight).
  • -

      If you press the middle mouse - button (the button changed in the tool options under Browser Field->Cursor Text - Highlight), the decompiler will highlight every occurrence of a variable or constant - under the current cursor location.
    -

    -
    +
    +

    You can navigate to the target + of a goto statement by double-clicking its label (you can also double-click + a brace to navigate to the matching brace).
    +

    +
    + +

    diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/DecompilerLocation.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/DecompilerLocation.java index cdaac8131f..8d4bfd18bb 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/DecompilerLocation.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/DecompilerLocation.java @@ -15,6 +15,8 @@ */ package ghidra.app.decompiler; +import java.util.Objects; + import ghidra.framework.options.SaveState; import ghidra.program.model.address.Address; import ghidra.program.model.listing.Program; @@ -52,7 +54,7 @@ public class DecompilerLocation extends ProgramLocation { /** * Results from the decompilation * - * @return C-AST, DFG, and CFG object. Can return null if there are no results attached to this location. + * @return C-AST, DFG, and CFG object. null if there are no results attached to this location */ public DecompileResults getDecompile() { return results; @@ -85,29 +87,36 @@ public class DecompilerLocation extends ProgramLocation { @Override public boolean equals(Object obj) { - if (this == obj) + if (obj == null) { + return false; + } + if (this == obj) { return true; - if (!super.equals(obj)) + } + if (getClass() != obj.getClass()) { return false; - if (getClass() != obj.getClass()) + } + + if (!super.equals(obj)) { return false; + } + DecompilerLocation other = (DecompilerLocation) obj; - if (charPos != other.charPos) + if (charPos != other.charPos) { return false; - if (functionEntryPoint == null) { - if (other.functionEntryPoint != null) - return false; } - else if (!functionEntryPoint.equals(other.functionEntryPoint)) + + if (lineNumber != other.lineNumber) { return false; - if (lineNumber != other.lineNumber) - return false; - if (tokenName == null) { - if (other.tokenName != null) - return false; } - else if (!tokenName.equals(other.tokenName)) + + if (!Objects.equals(functionEntryPoint, other.functionEntryPoint)) { return false; + } + + if (!Objects.equals(tokenName, other.tokenName)) { + return false; + } return true; } @@ -137,4 +146,19 @@ public class DecompilerLocation extends ProgramLocation { public int getCharPos() { return charPos; } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(); + buf.append(getClass().getSimpleName()); + buf.append('@'); + buf.append(addr.toString()); + buf.append(", line="); + buf.append(lineNumber); + buf.append(", character="); + buf.append(charPos); + buf.append(", token="); + buf.append(tokenName); + return buf.toString(); + } } diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/ClangHighlightController.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/ClangHighlightController.java index fae0088253..4783afe98a 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/ClangHighlightController.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/ClangHighlightController.java @@ -15,22 +15,21 @@ */ package ghidra.app.decompiler.component; -import ghidra.app.decompiler.*; -import ghidra.program.model.pcode.PcodeOp; -import ghidra.program.model.pcode.Varnode; - import java.awt.Color; import java.util.*; import docking.widgets.EventTrigger; import docking.widgets.fieldpanel.field.Field; import docking.widgets.fieldpanel.support.FieldLocation; +import ghidra.app.decompiler.*; +import ghidra.program.model.pcode.PcodeOp; +import ghidra.program.model.pcode.Varnode; /** * Class to handle highlights for a decompiled function. */ -public class ClangHighlightController { +public abstract class ClangHighlightController { // Note: Most of the methods in this class were extracted from the ClangLayoutController class // and the DecompilerPanel class. @@ -40,14 +39,16 @@ public class ClangHighlightController { protected Color defaultSpecialColor = new Color(255, 100, 0, 128); // Default color for specially highlighted tokens protected Color defaultParenColor = new Color(255, 255, 0, 128); // Default color for highlighting parentheses - protected HashSet highlightTokenSet = new HashSet(); + protected HashSet highlightTokenSet = new HashSet<>(); - protected ArrayList highlightListenerList = - new ArrayList(); + protected ArrayList highlightListenerList = new ArrayList<>(); public ClangHighlightController() { } + public abstract void fieldLocationChanged(FieldLocation location, Field field, + EventTrigger trigger); + void loadOptions(DecompileOptions options) { Color currentVariableHighlightColor = options.getCurrentVariableHighlightColor(); if (currentVariableHighlightColor != null) { @@ -115,18 +116,21 @@ public class ClangHighlightController { else if (node instanceof ClangToken) { ClangToken tok = (ClangToken) node; Varnode vn = DecompilerUtils.getVarnodeRef(tok); - if (varnodes.contains(vn)) + if (varnodes.contains(vn)) { addHighlight(tok, highlightColor); + } if (vn == specificvn) { // Look for specific varnode to label with specialColor - if ((specificop != null) && (tok.getPcodeOp() == specificop)) + if ((specificop != null) && (tok.getPcodeOp() == specificop)) { addHighlight(tok, specialColor); + } } } } notifyListeners(); } - public void addPcodeOpsToHighlight(ClangNode parentNode, Set ops, Color highlightColor) { + public void addPcodeOpsToHighlight(ClangNode parentNode, Set ops, + Color highlightColor) { int nchild = parentNode.numChildren(); for (int i = 0; i < nchild; ++i) { ClangNode node = parentNode.Child(i); @@ -136,8 +140,9 @@ public class ClangHighlightController { else if (node instanceof ClangToken) { ClangToken tok = (ClangToken) node; PcodeOp op = tok.getPcodeOp(); - if (ops.contains(op)) + if (ops.contains(op)) { addHighlight(tok, highlightColor); + } } } notifyListeners(); @@ -188,24 +193,27 @@ public class ClangHighlightController { * @return a list of all tokens that were highlighted. */ public List addHighlightParen(ClangSyntaxToken tok, Color highlightColor) { - ArrayList tokenList = new ArrayList(); + ArrayList tokenList = new ArrayList<>(); int paren = tok.getOpen(); - if (paren == -1) + if (paren == -1) { paren = tok.getClose(); - if (paren == -1) + } + if (paren == -1) { return tokenList; // Not a parenthesis + } ClangNode par = tok.Parent(); while (par != null) { boolean outside = true; if (par instanceof ClangTokenGroup) { - ArrayList list = new ArrayList(); + ArrayList list = new ArrayList<>(); ((ClangTokenGroup) par).flatten(list); for (int i = 0; i < list.size(); ++i) { ClangToken tk = (ClangToken) list.get(i); if (tk instanceof ClangSyntaxToken) { ClangSyntaxToken syn = (ClangSyntaxToken) tk; - if (syn.getOpen() == paren) + if (syn.getOpen() == paren) { outside = false; + } else if (syn.getClose() == paren) { outside = true; addHighlight(syn, highlightColor); @@ -224,72 +232,20 @@ public class ClangHighlightController { } public void addHighlightBrace(ClangSyntaxToken token, Color highlightColor) { - ClangNode parent = token.Parent(); - String text = token.getText(); - if ("{".equals(text)) { - highlightBrace(token, parent, false, highlightColor); - return; - } - if ("}".equals(text)) { - highlightBrace(token, parent, true, highlightColor); - return; - } - notifyListeners(); - } - - private void highlightBrace(ClangSyntaxToken startToken, ClangNode parent, boolean forward, - Color highlightColor) { - List list = new ArrayList(); - parent.flatten(list); - - if (!forward) { - Collections.reverse(list); - } - - Stack braceStack = new Stack(); - for (int i = 0; i < list.size(); ++i) { - ClangToken token = (ClangToken) list.get(i); - if (token instanceof ClangSyntaxToken) { - ClangSyntaxToken syntaxToken = (ClangSyntaxToken) token; - - if (startToken == syntaxToken) { - // found our starting token, take the current value on the stack - ClangSyntaxToken matchingBrace = braceStack.pop(); - matchingBrace.setMatchingToken(true); - addHighlight(matchingBrace, highlightColor); - return; - } - - if (!isBrace(syntaxToken)) { - continue; - } - - if (braceStack.isEmpty()) { - braceStack.push(syntaxToken); - continue; - } - - ClangSyntaxToken lastToken = braceStack.peek(); - if (isMatchingBrace(lastToken, syntaxToken)) { - braceStack.pop(); - } - else { - braceStack.push(syntaxToken); - } - } + if (DecompilerUtils.isBrace(token)) { + highlightBrace(token, highlightColor); + notifyListeners(); } } - private boolean isBrace(ClangSyntaxToken token) { - String text = token.getText(); - return "{".equals(text) || "}".equals(text); - } + private void highlightBrace(ClangSyntaxToken startToken, Color highlightColor) { - private boolean isMatchingBrace(ClangSyntaxToken braceToken, ClangSyntaxToken otherBraceToken) { - String brace = braceToken.getText(); - String otherBrace = otherBraceToken.getText(); - return !brace.equals(otherBrace); + ClangSyntaxToken matchingBrace = DecompilerUtils.getMatchingBrace(startToken); + if (matchingBrace != null) { + matchingBrace.setMatchingToken(true); // this is a signal to the painter + addHighlight(matchingBrace, highlightColor); + } } /** @@ -298,13 +254,14 @@ public class ClangHighlightController { */ public void addHighlightFill() { ClangTokenGroup lastgroup = null; - ArrayList newhi = new ArrayList(); - ArrayList newcolor = new ArrayList(); + ArrayList newhi = new ArrayList<>(); + ArrayList newcolor = new ArrayList<>(); for (ClangToken tok : highlightTokenSet) { if (tok.Parent() instanceof ClangTokenGroup) { ClangTokenGroup par = (ClangTokenGroup) tok.Parent(); - if (par == lastgroup) + if (par == lastgroup) { continue; + } lastgroup = par; int beg = -1; int end = par.numChildren(); @@ -313,8 +270,9 @@ public class ClangHighlightController { ClangToken token = (ClangToken) par.Child(j); Color curcolor = token.getHighlight(); if (curcolor != null) { - if (beg == -1) + if (beg == -1) { beg = j; + } else { end = j; for (int k = beg + 1; k < end; ++k) { @@ -330,45 +288,22 @@ public class ClangHighlightController { beg = -1; } } - else + else { beg = -1; + } } } } for (int i = 0; i < newhi.size(); ++i) { ClangToken tok = (ClangToken) newhi.get(i); - if (tok.getHighlight() != null) + if (tok.getHighlight() != null) { continue; + } addHighlight(tok, newcolor.get(i)); } notifyListeners(); } - public void fieldLocationChanged(FieldLocation location, Field field, EventTrigger trigger) { - - // Do nothing. - -// clearHighlights(); -// -// if (!(field instanceof ClangTextField)) { -// return; -// } -// -// ClangToken tok = ((ClangTextField) field).getToken(location); -// if (tok == null) { -// return; -// } -// -//// // clear any highlighted searchResults -//// decompilerPanel.setSearchResults(null); -// -// addHighlight(tok, defaultHighlightColor); -// if (tok instanceof ClangSyntaxToken) { -// addHighlightParen((ClangSyntaxToken) tok, defaultParenColor); -// addHighlightBrace((ClangSyntaxToken) tok, defaultParenColor); -// } - } - public boolean addListener(ClangHighlightListener listener) { return highlightListenerList.add(listener); } diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerPanel.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerPanel.java index 0a06f0f9d9..0cfedee1df 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerPanel.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerPanel.java @@ -25,6 +25,8 @@ import javax.swing.JComponent; import javax.swing.JPanel; import docking.DockingUtils; +import docking.util.AnimationUtils; +import docking.util.SwingAnimationCallback; import docking.widgets.EventTrigger; import docking.widgets.SearchLocation; import docking.widgets.fieldpanel.FieldPanel; @@ -48,9 +50,8 @@ import ghidra.util.*; import ghidra.util.bean.field.AnnotatedTextFieldElement; /** - * Class to handle the display of a decompiled function. + * Class to handle the display of a decompiled function */ - public class DecompilerPanel extends JPanel implements FieldMouseListener, FieldLocationListener, FieldSelectionListener, ClangHighlightListener { @@ -62,7 +63,7 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field private final DecompilerController controller; private final DecompileOptions options; - private FieldPanel codeViewer; + private DecompilerFieldPanel fieldPanel; private ClangLayoutController layoutMgr; private HighlightFactory hlFactory; private ClangHighlightController highlightController; @@ -93,13 +94,13 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field hlFactory = new SearchHighlightFactory(); layoutMgr = new ClangLayoutController(options, this, metrics, hlFactory); - codeViewer = new FieldPanel(layoutMgr); + fieldPanel = new DecompilerFieldPanel(layoutMgr); setBackground(options.getCodeViewerBackgroundColor()); - IndexedScrollPane scroller = new IndexedScrollPane(codeViewer); - codeViewer.addFieldSelectionListener(this); - codeViewer.addFieldMouseListener(this); - codeViewer.addFieldLocationListener(this); + IndexedScrollPane scroller = new IndexedScrollPane(fieldPanel); + fieldPanel.addFieldSelectionListener(this); + fieldPanel.addFieldMouseListener(this); + fieldPanel.addFieldLocationListener(this); decompilerHoverProvider = new DecompilerHoverProvider(); @@ -124,7 +125,7 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field } public FieldPanel getFieldPanel() { - return codeViewer; + return fieldPanel; } @Override @@ -133,15 +134,15 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field if (useNonFunctionColor) { bg = NON_FUNCTION_BACKGROUND_COLOR_DEF; } - if (codeViewer != null) { - codeViewer.setBackgroundColor(bg); + if (fieldPanel != null) { + fieldPanel.setBackgroundColor(bg); } super.setBackground(bg); } /** - * This function sets the current window display based - * on our display state. + * This function sets the current window display based on our display state + * @param decompileData the new data */ void setDecompileData(DecompileData decompileData) { if (layoutMgr == null) { @@ -207,7 +208,7 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field return; } if (viewerPosition != null) { - codeViewer.setViewerPosition(viewerPosition.getIndex(), viewerPosition.getXOffset(), + fieldPanel.setViewerPosition(viewerPosition.getIndex(), viewerPosition.getXOffset(), viewerPosition.getYOffset()); } List tokens = @@ -215,25 +216,79 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field if (location instanceof DecompilerLocation) { DecompilerLocation decompilerLocation = (DecompilerLocation) location; - codeViewer.goTo(BigInteger.valueOf(decompilerLocation.getLineNumber()), 0, 0, + fieldPanel.goTo(BigInteger.valueOf(decompilerLocation.getLineNumber()), 0, 0, decompilerLocation.getCharPos(), false); } else if (!tokens.isEmpty()) { - int firstfield = DecompilerUtils.findIndexOfFirstField(tokens, layoutMgr.getFields()); - // Put cursor on first token in the list - if (firstfield != -1) { - codeViewer.goTo(BigInteger.valueOf(firstfield), 0, 0, 0, false); - } + goToBeginningOfLine(tokens); } } /** - * Translate Ghidra address to decompiler address. - * Functions within an overlay space are decompiled - * in their physical space, therefore decompiler results - * refer to the functions underlying .physical space - * @param addr - * @return + * Put cursor on first token in the list + * @param tokens the tokens to search for + */ + private void goToBeginningOfLine(List tokens) { + int firstLineNumber = DecompilerUtils.findIndexOfFirstField(tokens, layoutMgr.getFields()); + if (firstLineNumber != -1) { + fieldPanel.goTo(BigInteger.valueOf(firstLineNumber), 0, 0, 0, false); + } + } + + private void goToToken(ClangToken token) { + + ClangLine line = token.getLineParent(); + + int offset = 0; + List tokens = line.getAllTokens(); + for (ClangToken lineToken : tokens) { + if (lineToken.equals(token)) { + break; + } + offset += lineToken.getText().length(); + } + + // -1 since the FieldPanel is 0-based; we are 1-based + int lineNumber = line.getLineNumber() - 1; + int column = offset; + FieldLocation start = getCursorPosition(); + + int distance = getOffscreenDistance(lineNumber); + if (distance == 0) { + fieldPanel.navigateTo(lineNumber, column); + return; + } + + ScrollingCallback callback = new ScrollingCallback(start, lineNumber, column, distance); + AnimationUtils.executeSwingAnimationCallback(callback); + } + + private int getOffscreenDistance(int line) { + + AnchoredLayout start = fieldPanel.getVisibleStartLayout(); + int visibleStartLine = start.getIndex().intValue(); + if (visibleStartLine > line) { + // the end is off the top of the screen + return visibleStartLine - line; + } + + AnchoredLayout end = fieldPanel.getVisibleEndLayout(); + int visibleEndLine = end.getIndex().intValue(); + if (visibleEndLine < line) { + // the end is off the bottom of the screen + return line - visibleEndLine; + } + + return 0; + } + + /** + * Translate Ghidra address to decompiler address. Functions within an overlay space are + * decompiled in their physical space, therefore decompiler results refer to the + * functions underlying .physical space + * + * @param addr the Ghidra address + * @return the decompiler address */ private Address translateAddress(Address addr) { Function func = decompileData.getFunction(); @@ -248,12 +303,12 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field } /** - * Translate Ghidra address set to decompiler address set. - * Functions within an overlay space are decompiled - * in their physical space, therefore decompiler results + * Translate Ghidra address set to decompiler address set. Functions within an overlay + * space are decompiled in their physical space, therefore decompiler results * refer to the functions underlying .physical space - * @param set - * @return + * + * @param set the Ghidra addresses + * @return the decompiler addresses */ private AddressSetView translateSet(AddressSetView set) { Function func = decompileData.getFunction(); @@ -288,7 +343,7 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field DecompilerUtils.getTokens(layoutMgr.getRoot(), translateSet(selection)); fieldSelection = DecompilerUtils.getFieldSelection(tokens); } - codeViewer.setSelection(fieldSelection); + fieldPanel.setSelection(fieldSelection); } public void setDecompilerHoverProvider(DecompilerHoverProvider provider) { @@ -363,45 +418,84 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field ClangTextField textField = (ClangTextField) field; ClangToken token = textField.getToken(location); if (token instanceof ClangFuncNameToken) { - Function function = - DecompilerUtils.getFunction(controller.getProgram(), (ClangFuncNameToken) token); - if (function != null) { - controller.goToFunction(function, newWindow); - } - else { - String labelName = token.getText(); - if (labelName.startsWith("func_0x")) { - try { - Address addr = decompileData.getFunction().getEntryPoint().getAddress( - labelName.substring(7)); - controller.goToAddress(addr, newWindow); - } - catch (AddressFormatException e) { - controller.goToLabel(labelName, newWindow); - } - } - } + tryGoToFunction(token, newWindow); } else if (token instanceof ClangLabelToken) { - Address addr = token.getMinAddress(); - controller.goToAddress(addr, newWindow); + tryGoToLabel((ClangLabelToken) token, newWindow); } else if (token instanceof ClangVariableToken) { tryGoToVarnode((ClangVariableToken) token, newWindow); } else if (token instanceof ClangCommentToken) { - // special cases - // -comments: these no longer use tokens for each item, but are one composite field - FieldElement clickedElement = textField.getClickedObject(location); - if (clickedElement instanceof AnnotatedTextFieldElement) { - AnnotatedTextFieldElement annotation = (AnnotatedTextFieldElement) clickedElement; - controller.annotationClicked(annotation, event, newWindow); + tryGoToComment(location, event, textField, token, newWindow); + } + else if (token instanceof ClangSyntaxToken) { + tryGoToSyntaxToken((ClangSyntaxToken) token); + } + } + + private void tryGoToComment(FieldLocation location, MouseEvent event, ClangTextField textField, + ClangToken token, boolean newWindow) { + + // special cases + // -comments: these no longer use tokens for each item, but are one composite field + FieldElement clickedElement = textField.getClickedObject(location); + if (clickedElement instanceof AnnotatedTextFieldElement) { + AnnotatedTextFieldElement annotation = (AnnotatedTextFieldElement) clickedElement; + controller.annotationClicked(annotation, event, newWindow); + return; + } + + String text = clickedElement.getText(); + String word = StringUtilities.findWord(text, location.col); + tryGoToScalar(word, newWindow); + } + + private void tryGoToFunction(ClangToken token, boolean newWindow) { + Function function = + DecompilerUtils.getFunction(controller.getProgram(), (ClangFuncNameToken) token); + if (function != null) { + controller.goToFunction(function, newWindow); + return; + } + + // TODO no idea what this is supposed to be handling...someone doc this please + String labelName = token.getText(); + if (labelName.startsWith("func_0x")) { + try { + Address addr = + decompileData.getFunction().getEntryPoint().getAddress(labelName.substring(7)); + controller.goToAddress(addr, newWindow); + } + catch (AddressFormatException e) { + controller.goToLabel(labelName, newWindow); + } + } + } + + private void tryGoToLabel(ClangLabelToken token, boolean newWindow) { + ClangNode node = token.Parent(); + if (node instanceof ClangStatement) { + // check for a goto label + ClangTokenGroup root = layoutMgr.getRoot(); + ClangLabelToken destination = DecompilerUtils.getGoToTargetToken(root, token); + if (destination != null) { + goToToken(destination); return; } + } - String text = clickedElement.getText(); - String word = StringUtilities.findWord(text, location.col); - tryGoToScalar(word, newWindow); + Address addr = token.getMinAddress(); + controller.goToAddress(addr, newWindow); + } + + private void tryGoToSyntaxToken(ClangSyntaxToken token) { + + if (DecompilerUtils.isBrace(token)) { + ClangSyntaxToken otherBrace = DecompilerUtils.getMatchingBrace(token); + if (otherBrace != null) { + goToToken(otherBrace); + } } } @@ -490,8 +584,8 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field if (!decompileData.hasDecompileResults()) { return null; } - Field currentField = codeViewer.getCurrentField(); - FieldLocation cursorPosition = codeViewer.getCursorLocation(); + Field currentField = fieldPanel.getCurrentField(); + FieldLocation cursorPosition = fieldPanel.getCursorLocation(); return getProgramLocation(currentField, cursorPosition); } @@ -514,6 +608,7 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field return; } + // only broadcast when the user is clicking around if (trigger == EventTrigger.GUI_ACTION) { ProgramLocation programLocation = getProgramLocation(field, location); if (programLocation != null) { @@ -605,13 +700,13 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field } public FieldLocation getCursorPosition() { - return codeViewer.getCursorLocation(); + return fieldPanel.getCursorLocation(); } public void setCursorPosition(FieldLocation fieldLocation) { - codeViewer.setCursorPosition(fieldLocation.getIndex(), fieldLocation.getFieldNum(), + fieldPanel.setCursorPosition(fieldLocation.getIndex(), fieldLocation.getFieldNum(), fieldLocation.getRow(), fieldLocation.getCol()); - codeViewer.scrollToCursor(); + fieldPanel.scrollToCursor(); } /** @@ -619,7 +714,7 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field * @return a single selected token; null if there is no selection or multiple tokens selected. */ public ClangToken getSelectedToken() { - FieldSelection selection = codeViewer.getSelection(); + FieldSelection selection = fieldPanel.getSelection(); if (selection.isEmpty()) { return null; } @@ -635,8 +730,8 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field } public ClangToken getTokenAtCursor() { - FieldLocation cursorPosition = codeViewer.getCursorLocation(); - Field field = codeViewer.getCurrentField(); + FieldLocation cursorPosition = fieldPanel.getCursorLocation(); + Field field = fieldPanel.getCurrentField(); if (field == null) { return null; } @@ -654,10 +749,10 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field public void setHoverMode(boolean enabled) { decompilerHoverProvider.setHoverEnabled(enabled); if (enabled) { - codeViewer.setHoverProvider(decompilerHoverProvider); + fieldPanel.setHoverProvider(decompilerHoverProvider); } else { - codeViewer.setHoverProvider(null); + fieldPanel.setHoverProvider(null); } } @@ -702,49 +797,25 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field } } - class SearchHighlightFactory implements HighlightFactory { - - @Override - public Highlight[] getHighlights(Field field, String text, int cursorTextOffset) { - if (currentSearchLocation == null) { - return new Highlight[0]; - } - - ClangTextField cField = (ClangTextField) field; - int highlightLine = cField.getLineNumber(); - - FieldLocation searchCursorLocation = - ((FieldBasedSearchLocation) currentSearchLocation).getFieldLocation(); - int searchLineNumber = searchCursorLocation.getIndex().intValue() + 1; - if (highlightLine != searchLineNumber) { - // only highlight the match on the actual line - return new Highlight[0]; - } - - return new Highlight[] { new Highlight(currentSearchLocation.getStartIndexInclusive(), - currentSearchLocation.getEndIndexInclusive(), currentSearchHighlightColor) }; - } - } - public ViewerPosition getViewerPosition() { - return codeViewer.getViewerPosition(); + return fieldPanel.getViewerPosition(); } public void setViewerPosition(ViewerPosition viewerPosition) { - codeViewer.setViewerPosition(viewerPosition.getIndex(), viewerPosition.getXOffset(), + fieldPanel.setViewerPosition(viewerPosition.getIndex(), viewerPosition.getXOffset(), viewerPosition.getYOffset()); } @Override public void requestFocus() { - codeViewer.requestFocus(); + fieldPanel.requestFocus(); } public void selectAll() { BigInteger numIndexes = layoutMgr.getNumIndexes(); FieldSelection selection = new FieldSelection(); selection.addRange(BigInteger.ZERO, numIndexes); - codeViewer.setSelection(selection); + fieldPanel.setSelection(selection); // fake it out that the selection was caused by the field panel GUI. selectionChanged(selection, EventTrigger.GUI_ACTION); } @@ -770,4 +841,102 @@ public class DecompilerPanel extends JPanel implements FieldMouseListener, Field repaint(); } +//================================================================================================== +// Inner Classes +//================================================================================================== + + private class SearchHighlightFactory implements HighlightFactory { + + @Override + public Highlight[] getHighlights(Field field, String text, int cursorTextOffset) { + if (currentSearchLocation == null) { + return new Highlight[0]; + } + + ClangTextField cField = (ClangTextField) field; + int highlightLine = cField.getLineNumber(); + + FieldLocation searchCursorLocation = + ((FieldBasedSearchLocation) currentSearchLocation).getFieldLocation(); + int searchLineNumber = searchCursorLocation.getIndex().intValue() + 1; + if (highlightLine != searchLineNumber) { + // only highlight the match on the actual line + return new Highlight[0]; + } + + return new Highlight[] { new Highlight(currentSearchLocation.getStartIndexInclusive(), + currentSearchLocation.getEndIndexInclusive(), currentSearchHighlightColor) }; + } + } + + /** + * A simple class that handles the animators callback to scroll the display + */ + private class ScrollingCallback implements SwingAnimationCallback { + + private int startLine; + private int endLine; + private int endColumn; + private int duration; + + ScrollingCallback(FieldLocation start, int endLineNumber, int endColumn, int distance) { + this.startLine = start.getIndex().intValue(); + this.endLine = endLineNumber; + this.endColumn = endColumn; + + // have things nearby execute more quickly so users don't wait needlessly + double rate = Math.pow(distance, .8); + int ms = (int) rate * 100; + this.duration = Math.min(1000, ms); + } + + @Override + public int getDuration() { + return duration; + } + + @Override + public void progress(double percentComplete) { + + int length = Math.abs(endLine - startLine); + long offset = Math.round(length * percentComplete); + int current = 0; + if (startLine > endLine) { + // backwards + current = (int) (startLine - offset); + } + else { + current = (int) (startLine + offset); + } + + FieldLocation location = new FieldLocation(BigInteger.valueOf(current)); + fieldPanel.scrollTo(location); + } + + @Override + public void done() { + fieldPanel.goTo(BigInteger.valueOf(endLine), 0, 0, endColumn, false); + } + } + + private class DecompilerFieldPanel extends FieldPanel { + + public DecompilerFieldPanel(LayoutModel model) { + super(model); + } + + /** + * Moves this field panel to the given line and column. Further, this navigation will + * fire an event to the rest of the tool. (This is in contrast to a field panel + * goTo, which we use to simply move the cursor, but not trigger an + * tool-level navigation event.) + * + * @param lineNumber the line number + * @param column the column within the line + */ + void navigateTo(int lineNumber, int column) { + fieldPanel.goTo(BigInteger.valueOf(lineNumber), 0, 0, column, false, + EventTrigger.GUI_ACTION); + } + } } diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerUtils.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerUtils.java index e7df8938f0..461f49fa8a 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerUtils.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/decompiler/component/DecompilerUtils.java @@ -234,16 +234,17 @@ public class DecompilerUtils { /** * Find index of first field containing a ClangNode in tokenList - * @param tokenlist + * @param queryTokens the list of tokens of interest + * @param fields the universe of fields to check * @return index of field, or -1 */ - public static int findIndexOfFirstField(List tokenlist, Field[] fields) { + public static int findIndexOfFirstField(List queryTokens, Field[] fields) { for (int i = 0; i < fields.length; i++) { ClangTextField f = (ClangTextField) fields[i]; - List tokenList = f.getTokens(); - for (int j = 0; j < tokenList.size(); j++) { - ClangNode token = tokenList.get(j); - if (tokenlist.contains(token)) { + List fieldTokens = f.getTokens(); + for (int j = 0; j < fieldTokens.size(); j++) { + ClangNode fieldToken = fieldTokens.get(j); + if (queryTokens.contains(fieldToken)) { return i; } } @@ -252,11 +253,10 @@ public class DecompilerUtils { } /** - * Find all ClangNodes that have a minimum address in - * the AddressSetView - * @param reslist is resulting list of found ClangNodes - * @param parentNode is root of node tree to search - * @param aset is the AddressSetView to match + * Find all ClangNodes that have a minimum address in the AddressSetView + * @param root the root of the token tree + * @param addressSet the addresses to restrict + * @return the list of tokens */ public static List getTokens(ClangNode root, AddressSetView addressSet) { List tokenList = new ArrayList<>(); @@ -512,6 +512,110 @@ public class DecompilerUtils { return null; } + public static ClangLabelToken getGoToTargetToken(ClangTokenGroup root, ClangLabelToken label) { + ClangNode parent = label.Parent(); + if (!(parent instanceof ClangStatement)) { + return null; + } + + ClangStatement statement = (ClangStatement) parent; + if (!isGoToStatement(statement)) { + return null; + } + + String destinationStart = label.getText() + ':'; + Address address = label.getMinAddress(); + List tokens = DecompilerUtils.getTokens(root, address); + for (ClangToken token : tokens) { + if (isGoToStatement(token)) { + continue; // ignore any goto statements + } + + if (!(token instanceof ClangLabelToken)) { + continue; + } + + ClangNode tokenParent = token.Parent(); + String parentText = tokenParent.toString(); + if (parentText.startsWith(destinationStart)) { + return (ClangLabelToken) token; + } + } + + return null; + } + + public static ClangSyntaxToken getMatchingBrace(ClangSyntaxToken startToken) { + + ClangNode parent = startToken.Parent(); + List list = new ArrayList<>(); + parent.flatten(list); + + String text = startToken.getText(); + boolean forward = "}".equals(text); + if (!forward) { + Collections.reverse(list); + } + + Stack braceStack = new Stack<>(); + for (int i = 0; i < list.size(); ++i) { + ClangToken token = (ClangToken) list.get(i); + if (token instanceof ClangSyntaxToken) { + ClangSyntaxToken syntaxToken = (ClangSyntaxToken) token; + + if (startToken == syntaxToken) { + // found our starting token, take the current value on the stack + ClangSyntaxToken matchingBrace = braceStack.pop(); + return matchingBrace; + } + + if (!isBrace(syntaxToken)) { + continue; + } + + if (braceStack.isEmpty()) { + braceStack.push(syntaxToken); + continue; + } + + ClangSyntaxToken lastToken = braceStack.peek(); + if (isMatchingBrace(lastToken, syntaxToken)) { + braceStack.pop(); + } + else { + braceStack.push(syntaxToken); + } + } + } + return null; + } + + public static boolean isMatchingBrace(ClangSyntaxToken braceToken, + ClangSyntaxToken otherBraceToken) { + String brace = braceToken.getText(); + String otherBrace = otherBraceToken.getText(); + return !brace.equals(otherBrace); + } + + public static boolean isBrace(ClangSyntaxToken token) { + String text = token.getText(); + return "{".equals(text) || "}".equals(text); + } + + public static boolean isGoToStatement(ClangToken token) { + + ClangNode parent = token.Parent(); + if (!(parent instanceof ClangStatement)) { + return false; + } + return isGoToStatement((ClangStatement) parent); + } + + private static boolean isGoToStatement(ClangStatement statement) { + String text = statement.toString(); + return text.startsWith("goto"); + } + public static ArrayList toLines(ClangTokenGroup group) { List alltoks = new ArrayList<>(); @@ -549,4 +653,5 @@ public class DecompilerUtils { lines.add(current); return lines; } + } diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/DecompilePlugin.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/DecompilePlugin.java index 2e50986413..8939c16d59 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/DecompilePlugin.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/DecompilePlugin.java @@ -79,12 +79,9 @@ public class DecompilePlugin extends Plugin { * This happens when a readDataState occurs when a tool is restored * or when switching program tabs. */ - SwingUpdateManager delayedLocationUpdateMgr = new SwingUpdateManager(200, 200, new Runnable() { - @Override - public void run() { - if (currentLocation != null) { - connectedProvider.setLocation(currentLocation, null); - } + SwingUpdateManager delayedLocationUpdateMgr = new SwingUpdateManager(200, 200, () -> { + if (currentLocation != null) { + connectedProvider.setLocation(currentLocation, null); } }); @@ -92,7 +89,7 @@ public class DecompilePlugin extends Plugin { super(tool); - disconnectedProviders = new ArrayList(); + disconnectedProviders = new ArrayList<>(); connectedProvider = new PrimaryDecompilerProvider(this); createActions(); diff --git a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/DecompilerProvider.java b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/DecompilerProvider.java index c3005ecfbc..a9a7d2d89b 100644 --- a/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/DecompilerProvider.java +++ b/Ghidra/Features/Decompiler/src/main/java/ghidra/app/plugin/core/decompile/DecompilerProvider.java @@ -443,6 +443,9 @@ public class DecompilerProvider extends NavigatableComponentProviderAdapter @Override public void locationChanged(ProgramLocation programLocation) { + if (programLocation.equals(currentLocation)) { + return; + } currentLocation = programLocation; contextChanged(); plugin.locationChanged(this, programLocation); diff --git a/Ghidra/Features/Decompiler/src/test.slow/java/ghidra/app/plugin/core/decompile/DecompilerNavigationTest.java b/Ghidra/Features/Decompiler/src/test.slow/java/ghidra/app/plugin/core/decompile/DecompilerNavigationTest.java new file mode 100644 index 0000000000..f8647cc293 --- /dev/null +++ b/Ghidra/Features/Decompiler/src/test.slow/java/ghidra/app/plugin/core/decompile/DecompilerNavigationTest.java @@ -0,0 +1,83 @@ +/* ### + * IP: GHIDRA + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package ghidra.app.plugin.core.decompile; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +import org.junit.Before; +import org.junit.Test; + +import ghidra.app.plugin.core.codebrowser.CodeViewerProvider; +import ghidra.program.model.listing.Program; +import ghidra.program.model.symbol.RefType; +import ghidra.program.model.symbol.SourceType; +import ghidra.program.util.OperandFieldLocation; +import ghidra.program.util.ProgramLocation; +import ghidra.test.ClassicSampleX86ProgramBuilder; + +public class DecompilerNavigationTest extends AbstractDecompilerTest { + + @Before + @Override + public void setUp() throws Exception { + super.setUp(); + + CodeViewerProvider cbProvider = codeBrowser.getProvider(); + tool.showComponentProvider(cbProvider, true); + } + + @Override + protected Program getProgram() throws Exception { + return buildProgram(); + } + + private Program buildProgram() throws Exception { + ClassicSampleX86ProgramBuilder builder = + new ClassicSampleX86ProgramBuilder("notepad", false, this); + + // need a default label at 01002cf0, so make up a reference + builder.createMemoryReference("01002ce5", "01002cf0", RefType.FALL_THROUGH, + SourceType.ANALYSIS); + + return builder.getProgram(); + } + + @Test + public void testNavigation_ExternalEventDoesNotTriggerNavigation() { + + // + // Test to make sure that external ProgramLocationEvent notifications to not trigger + // the Decompiler to broadcast a new event. Setup a tool with the Listing and + // the Decompiler open. Then, navigate in the Listing and verify the address does not + // move. (This is somewhat subject to the Code Unit at the address in how the + // Decompiler itself responds to the incoming event.) + // + + // very specific location within the instruction that is known to affect how the + // decompiler responds + String operandPrefix = "dword ptr [EBP + "; + String operandReferenceName = "destStr]"; + OperandFieldLocation operandLocation = new OperandFieldLocation(program, addr("0100416c"), + null, addr("0x8"), operandPrefix + operandReferenceName, 1, 9); + codeBrowser.goTo(operandLocation); + waitForSwing(); + + ProgramLocation currentLocation = codeBrowser.getCurrentLocation(); + assertTrue(currentLocation instanceof OperandFieldLocation); + assertEquals(operandLocation.getAddress(), currentLocation.getAddress()); + } +} diff --git a/Ghidra/Features/Decompiler/src/test.slow/java/ghidra/app/plugin/core/decompile/DecompilerTest.java b/Ghidra/Features/Decompiler/src/test.slow/java/ghidra/app/plugin/core/decompile/DecompilerTest.java index 7121e1bbb9..1044a1a4e8 100644 --- a/Ghidra/Features/Decompiler/src/test.slow/java/ghidra/app/plugin/core/decompile/DecompilerTest.java +++ b/Ghidra/Features/Decompiler/src/test.slow/java/ghidra/app/plugin/core/decompile/DecompilerTest.java @@ -23,16 +23,12 @@ import ghidra.program.model.listing.Function; import ghidra.program.model.listing.Program; import ghidra.test.AbstractGhidraHeadedIntegrationTest; import ghidra.test.ToyProgramBuilder; -import ghidra.util.task.TaskMonitorAdapter; +import ghidra.util.task.TaskMonitor; public class DecompilerTest extends AbstractGhidraHeadedIntegrationTest { private Program prog; private DecompInterface decompiler; - public DecompilerTest() { - super(); - } - @Before public void setUp() throws Exception { @@ -57,9 +53,8 @@ public class DecompilerTest extends AbstractGhidraHeadedIntegrationTest { public void testDecompileInterfaceReturnsAFunction() throws Exception { Address addr = prog.getAddressFactory().getDefaultAddressSpace().getAddress(0x0); Function func = prog.getListing().getFunctionAt(addr); - DecompileResults decompResults = - decompiler.decompileFunction(func, DecompileOptions.SUGGESTED_DECOMPILE_TIMEOUT_SECS, - TaskMonitorAdapter.DUMMY_MONITOR); + DecompileResults decompResults = decompiler.decompileFunction(func, + DecompileOptions.SUGGESTED_DECOMPILE_TIMEOUT_SECS, TaskMonitor.DUMMY); String decompilation = decompResults.getDecompiledFunction().getC(); Assert.assertNotNull(decompilation); } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/util/AnimationUtils.java b/Ghidra/Framework/Docking/src/main/java/docking/util/AnimationUtils.java index cf1113ec4d..678fca57b6 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/util/AnimationUtils.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/util/AnimationUtils.java @@ -220,8 +220,8 @@ public class AnimationUtils { // note: instead of checking for 'animationEnabled' here, it will happen in the driver // so that the we can call SwingAnimationCallback.done(), which will let the client // perform its final action. - - SwingAnimationCallbackDriver driver = new SwingAnimationCallbackDriver(callback, 1000); + int duration = callback.getDuration(); + SwingAnimationCallbackDriver driver = new SwingAnimationCallbackDriver(callback, duration); return driver.animator; } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/util/SwingAnimationCallback.java b/Ghidra/Framework/Docking/src/main/java/docking/util/SwingAnimationCallback.java index 197dd1537b..224e187b61 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/util/SwingAnimationCallback.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/util/SwingAnimationCallback.java @@ -1,6 +1,5 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,4 +34,14 @@ public interface SwingAnimationCallback { * finalization work. */ public void done(); + + /** + * Returns the duration of this callback. The default is 1000 ms. Subclasses + * can override this as needed. + * + * @return the duration + */ + public default int getDuration() { + return 1000; + } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/FieldPanel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/FieldPanel.java index 5b8059da8d..f1118a4016 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/FieldPanel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/FieldPanel.java @@ -197,7 +197,7 @@ public class FieldPanel extends JPanel @Override public void scrollLineDown() { - layouts = layoutHandler.ShiftViewDownOneRow(); + layouts = layoutHandler.shiftViewDownOneRow(); notifyScrollListenerViewChangedAndRepaint(); } @@ -295,6 +295,51 @@ public class FieldPanel extends JPanel return new ArrayList<>(layouts); } + /** + * Returns true if the given field location is rendered on the screen; false if scrolled + * offscreen + * + * @param location the location to check + * @return true if the location is on the screen + */ + public boolean isLocationVisible(FieldLocation location) { + if (location == null) { + return false; + } + + BigInteger locationIndex = location.getIndex(); + for (AnchoredLayout layout : layouts) { + if (layout.getIndex().equals(locationIndex)) { + return true; + } + } + return false; + } + + /** + * Returns the first visible layout or null if there are no visible layouts + * + * @return the first visible layout + */ + public AnchoredLayout getVisibleStartLayout() { + if (layouts.isEmpty()) { + return null; + } + return layouts.get(0); + } + + /** + * Returns the last visible layout or null if there are no visible layouts + * + * @return the last visible layout + */ + public AnchoredLayout getVisibleEndLayout() { + if (layouts.isEmpty()) { + return null; + } + return layouts.get(layouts.size() - 1); + } + @Override public void repaint() { repaintPosted = true; @@ -714,13 +759,19 @@ public class FieldPanel extends JPanel * the row in the field to go to. * @param col * the column in the field to go to. - * @param centerCursor + * @param alwaysCenterCursor * if true, centers cursor on screen. Otherwise, only centers * cursor if cursor is offscreen. */ public void goTo(BigInteger index, int fieldNum, int row, int col, boolean alwaysCenterCursor) { + goTo(index, fieldNum, row, col, alwaysCenterCursor, EventTrigger.API_CALL); + } - if (!cursorHandler.doSetCursorPosition(index, fieldNum, row, col, EventTrigger.API_CALL)) { + // for subclasses to control the event trigger + protected void goTo(BigInteger index, int fieldNum, int row, int col, + boolean alwaysCenterCursor, EventTrigger trigger) { + + if (!cursorHandler.doSetCursorPosition(index, fieldNum, row, col, trigger)) { return; } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/internal/AnchoredLayoutHandler.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/internal/AnchoredLayoutHandler.java index 6344155de0..9ae9cbdcfc 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/internal/AnchoredLayoutHandler.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/fieldpanel/internal/AnchoredLayoutHandler.java @@ -1,6 +1,5 @@ /* ### * IP: GHIDRA - * REVIEWED: YES * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,14 +26,15 @@ public class AnchoredLayoutHandler { private final LayoutModel model; private int viewHeight; - private final LinkedList layouts = new LinkedList(); - + private final LinkedList layouts = new LinkedList<>(); + public AnchoredLayoutHandler(LayoutModel model, int viewHeight) { - this.model = model; + this.model = model; this.viewHeight = viewHeight; } - - public List positionLayoutsAroundAnchor(BigInteger anchorIndex, int viewPosition) { + + public List positionLayoutsAroundAnchor(BigInteger anchorIndex, + int viewPosition) { layouts.clear(); AnchoredLayout layout = getClosestLayout(anchorIndex, viewPosition); @@ -42,12 +42,12 @@ public class AnchoredLayoutHandler { layouts.add(layout); fillOutLayouts(); } - return new ArrayList(layouts); + return new ArrayList<>(layouts); } - public List ShiftViewDownOneRow() { + public List shiftViewDownOneRow() { if (layouts.isEmpty()) { - return new ArrayList(); + return new ArrayList<>(); } AnchoredLayout layout = layouts.getFirst(); int yPos = layout.getYPos(); @@ -57,7 +57,7 @@ public class AnchoredLayoutHandler { public List shiftViewUpOneRow() { if (layouts.isEmpty()) { - return new ArrayList(); + return new ArrayList<>(); } int scrollAmount = 0; AnchoredLayout layout = layouts.getFirst(); @@ -67,7 +67,7 @@ public class AnchoredLayoutHandler { if (yPos == 0) { layout = getPreviousLayout(index, yPos); if (layout == null) { - return new ArrayList(layouts); + return new ArrayList<>(layouts); } layouts.add(0, layout); yPos = layout.getYPos(); @@ -77,20 +77,18 @@ public class AnchoredLayoutHandler { return shiftView(scrollAmount); } - - public List shiftViewDownOnePage() { if (layouts.isEmpty()) { - return new ArrayList(); + return new ArrayList<>(); } AnchoredLayout last = layouts.getLast(); - int diff = last.getScrollableUnitIncrement(viewHeight-last.getYPos(), -1); - return shiftView(viewHeight+diff); + int diff = last.getScrollableUnitIncrement(viewHeight - last.getYPos(), -1); + return shiftView(viewHeight + diff); } public List shiftViewUpOnePage() { if (layouts.isEmpty()) { - return new ArrayList(); + return new ArrayList<>(); } int scrollAmount = viewHeight; AnchoredLayout first = layouts.getFirst(); @@ -104,24 +102,24 @@ public class AnchoredLayoutHandler { first = layouts.getFirst(); if (first.getYPos() != 0) { - return ShiftViewDownOneRow(); + return shiftViewDownOneRow(); } - return new ArrayList(layouts); + return new ArrayList<>(layouts); } public List shiftView(int viewAmount) { repositionLayouts(-viewAmount); fillOutLayouts(); - return new ArrayList(layouts); + return new ArrayList<>(layouts); } public List setViewHeight(int viewHeight) { - this.viewHeight = viewHeight; + this.viewHeight = viewHeight; if (layouts.isEmpty()) { return positionLayoutsAroundAnchor(BigInteger.ZERO, 0); } fillOutLayouts(); - return new ArrayList(layouts); + return new ArrayList<>(layouts); } private void fillOutLayouts() { @@ -129,46 +127,46 @@ public class AnchoredLayoutHandler { return; } AnchoredLayout lastLayout = layouts.getLast(); - fillLayoutsForward(lastLayout.getIndex(), lastLayout.getYPos()+lastLayout.getHeight()); + fillLayoutsForward(lastLayout.getIndex(), lastLayout.getYPos() + lastLayout.getHeight()); lastLayout = layouts.getLast(); if (lastLayout.getEndY() < viewHeight) { repositionLayouts(viewHeight - lastLayout.getEndY()); } - + AnchoredLayout firstLayout = layouts.getFirst(); fillLayoutsBack(firstLayout.getIndex(), firstLayout.getYPos()); firstLayout = layouts.getFirst(); if (firstLayout.getYPos() > 0) { repositionLayouts(-firstLayout.getYPos()); } - + lastLayout = layouts.getLast(); - fillLayoutsForward(lastLayout.getIndex(), lastLayout.getYPos()+lastLayout.getHeight()); - + fillLayoutsForward(lastLayout.getIndex(), lastLayout.getYPos() + lastLayout.getHeight()); + trimLayouts(); } - + private void repositionLayouts(int amount) { for (AnchoredLayout layout : layouts) { - layout.setYPos(layout.getYPos()+amount); + layout.setYPos(layout.getYPos() + amount); } } - + private void trimLayouts() { Iterator it = layouts.iterator(); - while(it.hasNext()) { + while (it.hasNext()) { AnchoredLayout layout = it.next(); int y = layout.getYPos(); int height = layout.getHeight(); - if ( (y+height <= 0) || (y > viewHeight) ) { + if ((y + height <= 0) || (y > viewHeight)) { it.remove(); } } } - + private void fillLayoutsForward(BigInteger existingLastIndex, int y) { BigInteger index = existingLastIndex; - while(y < viewHeight) { + while (y < viewHeight) { AnchoredLayout nextLayout = getNextLayout(index, y); if (nextLayout == null) { return; @@ -178,9 +176,10 @@ public class AnchoredLayoutHandler { index = nextLayout.getIndex(); } } + private void fillLayoutsBack(BigInteger existingFirstIndex, int y) { BigInteger index = existingFirstIndex; - while(y > 0) { + while (y > 0) { AnchoredLayout prevLayout = getPreviousLayout(index, y); if (prevLayout == null) { return; @@ -192,17 +191,17 @@ public class AnchoredLayoutHandler { } private AnchoredLayout getPreviousLayout(BigInteger index, int yPos) { - while((index = model.getIndexBefore(index)) != null) { + while ((index = model.getIndexBefore(index)) != null) { Layout layout = model.getLayout(index); if (layout != null) { - return new AnchoredLayout(layout, index, yPos-layout.getHeight()); + return new AnchoredLayout(layout, index, yPos - layout.getHeight()); } } return null; } - + private AnchoredLayout getNextLayout(BigInteger index, int yPos) { - while((index = model.getIndexAfter(index)) != null) { + while ((index = model.getIndexAfter(index)) != null) { Layout layout = model.getLayout(index); if (layout != null) { return new AnchoredLayout(layout, index, yPos); @@ -210,7 +209,6 @@ public class AnchoredLayoutHandler { } return null; } - private AnchoredLayout getClosestLayout(BigInteger index, int y) { Layout layout = model.getLayout(index);