mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2024-11-22 12:11:55 +00:00
Merge remote-tracking branch 'origin/GT-2846-2847-dragonmacher-decompiler-navigation'
This commit is contained in:
commit
44e8f64d7e
@ -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<ListingModelListener> listeners = new ArrayList<>();
|
||||
|
||||
// Use a cache so that simple arrowing to-and-fro with the keyboard will respond quickly
|
||||
private LRUMap<Address, Layout> 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<RowLayout> 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
|
||||
|
@ -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());
|
||||
|
@ -56,10 +56,6 @@ public class OperandFieldFactoryTest extends AbstractGhidraHeadedIntegrationTest
|
||||
private Options fieldOptions;
|
||||
private Program program;
|
||||
|
||||
public OperandFieldFactoryTest() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
|
||||
|
@ -41,10 +41,6 @@ public class PostCommentFieldFactoryTest extends AbstractGhidraHeadedIntegration
|
||||
private Options fieldOptions;
|
||||
private Program program;
|
||||
|
||||
public PostCommentFieldFactoryTest() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
|
||||
|
@ -516,14 +516,21 @@
|
||||
<LI><B>Control-shift-click</B> - Triggers the Listing in a <A href=
|
||||
"help/topics/Snapshots/Snapshots.html">Snapshots</A> view to navigate to the address
|
||||
denoted by the symbol that was clicked.</LI>
|
||||
|
||||
<LI><B>Middle-mouse</B> - 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
|
||||
<B>Browser Field->Cursor Text Highlight</B>).</LI>
|
||||
</UL>
|
||||
|
||||
<P><IMG alt="" src="../../shared/note.png" border="0"> If you press the middle mouse
|
||||
button (the button changed in the tool options under <B>Browser Field->Cursor Text
|
||||
Highlight</B>), the decompiler will highlight every occurrence of a variable or constant
|
||||
under the current cursor location.<BR>
|
||||
</P>
|
||||
</BLOCKQUOTE><BR>
|
||||
<BLOCKQUOTE>
|
||||
<P><IMG alt="" src="../../shared/tip.png" border="0">You can navigate to the target
|
||||
of a <code>goto</code> statement by double-clicking its label (you can also double-click
|
||||
a brace to navigate to the matching brace).<BR>
|
||||
</P>
|
||||
</BLOCKQUOTE>
|
||||
</BLOCKQUOTE>
|
||||
<BR>
|
||||
<BR>
|
||||
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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<ClangToken> highlightTokenSet = new HashSet<ClangToken>();
|
||||
protected HashSet<ClangToken> highlightTokenSet = new HashSet<>();
|
||||
|
||||
protected ArrayList<ClangHighlightListener> highlightListenerList =
|
||||
new ArrayList<ClangHighlightListener>();
|
||||
protected ArrayList<ClangHighlightListener> 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<PcodeOp> ops, Color highlightColor) {
|
||||
public void addPcodeOpsToHighlight(ClangNode parentNode, Set<PcodeOp> 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<ClangToken> addHighlightParen(ClangSyntaxToken tok, Color highlightColor) {
|
||||
ArrayList<ClangToken> tokenList = new ArrayList<ClangToken>();
|
||||
ArrayList<ClangToken> 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<ClangNode> list = new ArrayList<ClangNode>();
|
||||
ArrayList<ClangNode> 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<ClangNode> list = new ArrayList<ClangNode>();
|
||||
parent.flatten(list);
|
||||
|
||||
if (!forward) {
|
||||
Collections.reverse(list);
|
||||
}
|
||||
|
||||
Stack<ClangSyntaxToken> braceStack = new Stack<ClangSyntaxToken>();
|
||||
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<ClangNode> newhi = new ArrayList<ClangNode>();
|
||||
ArrayList<Color> newcolor = new ArrayList<Color>();
|
||||
ArrayList<ClangNode> newhi = new ArrayList<>();
|
||||
ArrayList<Color> 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);
|
||||
}
|
||||
|
@ -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<ClangToken> 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<ClangToken> 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<ClangToken> 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
|
||||
* <code>goTo</code>, 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<ClangToken> tokenlist, Field[] fields) {
|
||||
public static int findIndexOfFirstField(List<ClangToken> queryTokens, Field[] fields) {
|
||||
for (int i = 0; i < fields.length; i++) {
|
||||
ClangTextField f = (ClangTextField) fields[i];
|
||||
List<ClangToken> tokenList = f.getTokens();
|
||||
for (int j = 0; j < tokenList.size(); j++) {
|
||||
ClangNode token = tokenList.get(j);
|
||||
if (tokenlist.contains(token)) {
|
||||
List<ClangToken> 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<ClangToken> getTokens(ClangNode root, AddressSetView addressSet) {
|
||||
List<ClangToken> 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<ClangToken> 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<ClangNode> list = new ArrayList<>();
|
||||
parent.flatten(list);
|
||||
|
||||
String text = startToken.getText();
|
||||
boolean forward = "}".equals(text);
|
||||
if (!forward) {
|
||||
Collections.reverse(list);
|
||||
}
|
||||
|
||||
Stack<ClangSyntaxToken> 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<ClangLine> toLines(ClangTokenGroup group) {
|
||||
|
||||
List<ClangNode> alltoks = new ArrayList<>();
|
||||
@ -549,4 +653,5 @@ public class DecompilerUtils {
|
||||
lines.add(current);
|
||||
return lines;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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<DecompilerProvider>();
|
||||
disconnectedProviders = new ArrayList<>();
|
||||
connectedProvider = new PrimaryDecompilerProvider(this);
|
||||
|
||||
createActions();
|
||||
|
@ -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);
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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 <code>1000 ms</code>. Subclasses
|
||||
* can override this as needed.
|
||||
*
|
||||
* @return the duration
|
||||
*/
|
||||
public default int getDuration() {
|
||||
return 1000;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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<AnchoredLayout> layouts = new LinkedList<AnchoredLayout>();
|
||||
private final LinkedList<AnchoredLayout> layouts = new LinkedList<>();
|
||||
|
||||
public AnchoredLayoutHandler(LayoutModel model, int viewHeight) {
|
||||
this.model = model;
|
||||
this.viewHeight = viewHeight;
|
||||
}
|
||||
|
||||
public List<AnchoredLayout> positionLayoutsAroundAnchor(BigInteger anchorIndex, int viewPosition) {
|
||||
public List<AnchoredLayout> 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<AnchoredLayout>(layouts);
|
||||
return new ArrayList<>(layouts);
|
||||
}
|
||||
|
||||
public List<AnchoredLayout> ShiftViewDownOneRow() {
|
||||
public List<AnchoredLayout> shiftViewDownOneRow() {
|
||||
if (layouts.isEmpty()) {
|
||||
return new ArrayList<AnchoredLayout>();
|
||||
return new ArrayList<>();
|
||||
}
|
||||
AnchoredLayout layout = layouts.getFirst();
|
||||
int yPos = layout.getYPos();
|
||||
@ -57,7 +57,7 @@ public class AnchoredLayoutHandler {
|
||||
|
||||
public List<AnchoredLayout> shiftViewUpOneRow() {
|
||||
if (layouts.isEmpty()) {
|
||||
return new ArrayList<AnchoredLayout>();
|
||||
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<AnchoredLayout>(layouts);
|
||||
return new ArrayList<>(layouts);
|
||||
}
|
||||
layouts.add(0, layout);
|
||||
yPos = layout.getYPos();
|
||||
@ -77,20 +77,18 @@ public class AnchoredLayoutHandler {
|
||||
return shiftView(scrollAmount);
|
||||
}
|
||||
|
||||
|
||||
|
||||
public List<AnchoredLayout> shiftViewDownOnePage() {
|
||||
if (layouts.isEmpty()) {
|
||||
return new ArrayList<AnchoredLayout>();
|
||||
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<AnchoredLayout> shiftViewUpOnePage() {
|
||||
if (layouts.isEmpty()) {
|
||||
return new ArrayList<AnchoredLayout>();
|
||||
return new ArrayList<>();
|
||||
}
|
||||
int scrollAmount = viewHeight;
|
||||
AnchoredLayout first = layouts.getFirst();
|
||||
@ -104,15 +102,15 @@ public class AnchoredLayoutHandler {
|
||||
|
||||
first = layouts.getFirst();
|
||||
if (first.getYPos() != 0) {
|
||||
return ShiftViewDownOneRow();
|
||||
return shiftViewDownOneRow();
|
||||
}
|
||||
return new ArrayList<AnchoredLayout>(layouts);
|
||||
return new ArrayList<>(layouts);
|
||||
}
|
||||
|
||||
public List<AnchoredLayout> shiftView(int viewAmount) {
|
||||
repositionLayouts(-viewAmount);
|
||||
fillOutLayouts();
|
||||
return new ArrayList<AnchoredLayout>(layouts);
|
||||
return new ArrayList<>(layouts);
|
||||
}
|
||||
|
||||
public List<AnchoredLayout> setViewHeight(int viewHeight) {
|
||||
@ -121,7 +119,7 @@ public class AnchoredLayoutHandler {
|
||||
return positionLayoutsAroundAnchor(BigInteger.ZERO, 0);
|
||||
}
|
||||
fillOutLayouts();
|
||||
return new ArrayList<AnchoredLayout>(layouts);
|
||||
return new ArrayList<>(layouts);
|
||||
}
|
||||
|
||||
private void fillOutLayouts() {
|
||||
@ -129,7 +127,7 @@ 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());
|
||||
@ -143,24 +141,24 @@ public class AnchoredLayoutHandler {
|
||||
}
|
||||
|
||||
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<AnchoredLayout> 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();
|
||||
}
|
||||
}
|
||||
@ -168,7 +166,7 @@ public class AnchoredLayoutHandler {
|
||||
|
||||
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);
|
||||
@ -211,7 +210,6 @@ public class AnchoredLayoutHandler {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private AnchoredLayout getClosestLayout(BigInteger index, int y) {
|
||||
Layout layout = model.getLayout(index);
|
||||
if (layout != null) {
|
||||
|
Loading…
Reference in New Issue
Block a user