Merge remote-tracking branch

'origin/GP-5720-dragonmacher-drop-down-field-contains-mode--SQUASHED'
(Closes #4725, Closes #8203)
This commit is contained in:
Ryan Kurtz 2025-08-19 12:59:16 -04:00
commit a48c081e61
35 changed files with 1629 additions and 676 deletions

View file

@ -317,6 +317,7 @@ src/main/help/help/topics/DataTypeEditors/images/BytesNumberInputDialog.png||GHI
src/main/help/help/topics/DataTypeEditors/images/Dialog.png||GHIDRA||||END|
src/main/help/help/topics/DataTypeEditors/images/Dialog_Create_Pointer.png||GHIDRA||||END|
src/main/help/help/topics/DataTypeEditors/images/Dialog_Multiple_Match.png||GHIDRA||||END|
src/main/help/help/topics/DataTypeEditors/images/Dialog_SearchMode.png||GHIDRA||||END|
src/main/help/help/topics/DataTypeEditors/images/Dialog_Select_Tree.png||GHIDRA||||END|
src/main/help/help/topics/DataTypeEditors/images/Dialog_Single_Match.png||GHIDRA||||END|
src/main/help/help/topics/DataTypeEditors/images/EnumEditor.png||GHIDRA||||END|

View file

@ -19,6 +19,59 @@
<P align="center"><IMG border="0" src="images/Dialog.png" alt=""><BR>
<I>Data Type Chooser Dialog</I></P>
<P>As you type text in the field, any potential matches will be displayed in the completion
window, which is described below.
</P>
<A NAME="SearchMode" />
<P>
The way matches are determined depends upon the search
mode you are in. The current mode is displayed at the right side of the text field,
indicated with a single character. Hovering over the character will show a tool tip
window that shows the name for the current mode.
</P>
<BLOCKQUOTE>
<BLOCKQUOTE>
<P><IMG border="0" src="help/shared/tip.png" alt="">To change the search mode, click on
the seach mode character at the right side of the text field.
</P>
<P>
You can also change the search mode using <B>Ctrl Down</B> and <B>Ctrl Up</B> to
change the mode forward and backward, respectively.
</P>
<P align="center"><IMG border="0" src="images/Dialog_SearchMode.png" alt=""><BR>
<I>Data Type Chooser Dialog</I></P>
</BLOCKQUOTE>
</BLOCKQUOTE>
<P>
By default, this chooser uses a <B>Starts With</B> matching mode. Any text typed will be
used to match all data type with a name that begins with the current search text.
</P>
<BLOCKQUOTE>
<BLOCKQUOTE>
<P><IMG border="0" src="help/shared/tip.png" alt="">This data type selection chooser
performs the best with the 'starts with' setting. For a large number of data types,
this is the recommended search setting.
</P>
</BLOCKQUOTE>
</BLOCKQUOTE>
<BLOCKQUOTE>
<BLOCKQUOTE>
<P><IMG border="0" src="help/shared/note.yellow.png" alt="">The text used to match is
based on the cursor position in the field. All text from the beginning up to the
cursor position will be used for the match. This allows you to arrow left and right
to control the matching list.
</P>
</BLOCKQUOTE>
</BLOCKQUOTE>
<H2><A name="completion"></A> Completion Window</H2>
<BLOCKQUOTE>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -18,8 +18,11 @@ package ghidra.app.plugin.core.function.editor;
import java.util.ArrayList;
import java.util.List;
import javax.help.UnsupportedOperationException;
import javax.swing.ListCellRenderer;
import org.apache.commons.lang3.StringUtils;
import docking.widgets.DropDownSelectionTextField;
import docking.widgets.DropDownTextFieldDataModel;
import docking.widgets.list.GListCellRenderer;
@ -37,6 +40,11 @@ public class RegisterDropDownSelectionDataModel implements DropDownTextFieldData
this.registers = registers;
}
@Override
public List<SearchMode> getSupportedSearchModes() {
return List.of(SearchMode.STARTS_WITH);
}
@Override
public ListCellRenderer<Register> getListRenderer() {
return new GListCellRenderer<Register>();
@ -54,11 +62,20 @@ public class RegisterDropDownSelectionDataModel implements DropDownTextFieldData
@Override
public List<Register> getMatchingData(String searchText) {
throw new UnsupportedOperationException(
"Method no longer supported. Instead, call getMatchingData(String, SearchMode)");
}
if (searchText == null || searchText.length() == 0) {
@Override
public List<Register> getMatchingData(String searchText, SearchMode searchMode) {
if (StringUtils.isBlank(searchText)) {
return registers;
}
if (searchMode != SearchMode.STARTS_WITH) {
throw new IllegalArgumentException("Unsupported SearchMode: " + searchMode);
}
searchText = searchText.toLowerCase();
List<Register> regList = new ArrayList<>();
@ -85,5 +102,4 @@ public class RegisterDropDownSelectionDataModel implements DropDownTextFieldData
}
return 0;
}
}

View file

@ -16,8 +16,6 @@
package ghidra.app.plugin.core.script;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.*;
import javax.swing.event.*;
@ -28,7 +26,6 @@ import docking.widgets.*;
import generic.theme.GThemeDefaults.Colors.Palette;
import ghidra.app.script.ScriptInfo;
import ghidra.util.HTMLUtilities;
import ghidra.util.UserSearchUtils;
/**
* A widget that allows the user to choose an existing script by typing its name or picking it
@ -222,24 +219,10 @@ public class ScriptSelectionEditor {
}
@Override
public List<ScriptInfo> getMatchingData(String searchText) {
// This pattern will: 1) allow users to match the typed text anywhere in the
// script names and 2) allow the use of globbing characters
Pattern pattern = UserSearchUtils.createContainsPattern(searchText, true,
Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
List<ScriptInfo> results = new ArrayList<>();
for (ScriptInfo info : data) {
String name = info.getName();
Matcher m = pattern.matcher(name);
if (m.matches()) {
results.add(info);
}
public List<SearchMode> getSupportedSearchModes() {
return List.of(SearchMode.CONTAINS, SearchMode.WILDCARD, SearchMode.STARTS_WITH);
}
return results;
}
}
private class ScriptSelectionTextField extends DropDownSelectionTextField<ScriptInfo> {

View file

@ -18,14 +18,18 @@ package ghidra.app.util.datatype;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.help.UnsupportedOperationException;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.event.*;
import javax.swing.tree.TreePath;
import org.apache.commons.lang3.StringUtils;
import docking.widgets.DropDownSelectionTextField;
import docking.widgets.DropDownTextFieldDataModel;
import docking.widgets.button.BrowseButton;
@ -407,15 +411,36 @@ public class CategoryPathSelectionEditor extends AbstractCellEditor {
}
@Override
public List<CategoryPath> getMatchingData(String searchText) {
if (searchText == null || searchText.length() == 0) {
return Collections.emptyList();
public List<SearchMode> getSupportedSearchModes() {
return List.of(SearchMode.CONTAINS, SearchMode.STARTS_WITH, SearchMode.WILDCARD);
}
@Override
public List<CategoryPath> getMatchingData(String searchText) {
throw new UnsupportedOperationException(
"Method no longer supported. Instead, call getMatchingData(String, SearchMode)");
}
@Override
public List<CategoryPath> getMatchingData(String searchText, SearchMode mode) {
if (StringUtils.isBlank(searchText)) {
return new ArrayList<>(data);
}
if (!getSupportedSearchModes().contains(mode)) {
throw new IllegalArgumentException("Unsupported SearchMode: " + mode);
}
Pattern p = mode.createPattern(searchText);
return getMatchingDataRegex(p);
}
private List<CategoryPath> getMatchingDataRegex(Pattern p) {
List<CategoryPath> results = new ArrayList<>();
for (CategoryPath path : data) {
String pathString = path.getPath();
if (pathString.contains(searchText)) {
Matcher m = p.matcher(pathString);
if (m.matches()) {
results.add(path);
}
}

View file

@ -17,7 +17,10 @@ package ghidra.app.util.datatype;
import java.awt.Component;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.help.UnsupportedOperationException;
import javax.swing.*;
import docking.widgets.DropDownSelectionTextField;
@ -69,6 +72,11 @@ public class DataTypeDropDownSelectionDataModel implements DropDownTextFieldData
return service;
}
@Override
public List<SearchMode> getSupportedSearchModes() {
return List.of(SearchMode.STARTS_WITH, SearchMode.CONTAINS, SearchMode.WILDCARD);
}
@Override
public ListCellRenderer<DataType> getListRenderer() {
return new DataTypeDropDownRenderer();
@ -86,13 +94,47 @@ public class DataTypeDropDownSelectionDataModel implements DropDownTextFieldData
@Override
public List<DataType> getMatchingData(String searchText) {
throw new UnsupportedOperationException(
"Method no longer supported. Instead, call getMatchingData(String, SearchMode)");
}
@Override
public List<DataType> getMatchingData(String searchText, SearchMode mode) {
if (searchText == null || searchText.length() == 0) {
// full list results not supported since the data may be too large for user interaction
return Collections.emptyList();
}
List<DataType> dataTypeList =
if (!getSupportedSearchModes().contains(mode)) {
throw new IllegalArgumentException("Unsupported SearchMode: " + mode);
}
if (mode == SearchMode.STARTS_WITH) {
return getMatchDataStartsWith(searchText);
}
Pattern p = mode.createPattern(searchText);
return getMatchingDataRegex(p);
}
private List<DataType> getMatchDataStartsWith(String searchText) {
List<DataType> results =
DataTypeUtils.getStartsWithMatchingDataTypes(searchText, dataTypeService);
return filterDataTypeList(dataTypeList);
return filterDataTypeList(results);
}
private List<DataType> getMatchingDataRegex(Pattern p) {
List<DataType> results = new ArrayList<>();
List<DataType> allTypes = dataTypeService.getSortedDataTypeList();
for (DataType dt : allTypes) {
String name = dt.getName().toLowerCase();
Matcher m = p.matcher(name);
if (m.matches()) {
results.add(dt);
}
}
return filterDataTypeList(results);
}
/**

View file

@ -151,6 +151,8 @@ public class DataTypeSelectionEditor extends AbstractCellEditor {
editorPanel.add(selectionField);
editorPanel.add(browsePanel);
// This listener is not installed under certain conditions, such as when
// setTabCommitsEdit(true) is called.
keyListener = new KeyAdapter() {
@Override

View file

@ -87,7 +87,7 @@ import resources.ResourceManager;
public abstract class AbstractScreenShotGenerator extends AbstractGhidraHeadedIntegrationTest {
private static final String SCREENSHOT_USER_NAME = "User-1";
protected static final String SCREENSHOT_USER_NAME = "User-1";
static {
System.setProperty("user.name", "User-1");

View file

@ -787,7 +787,7 @@ public abstract class AbstractFunctionGraphTest extends AbstractGhidraHeadedInte
waitForSwing();
int tryCount = 3;
int tryCount = 0;
while (tryCount++ < 5 && updater.isBusy()) {
waitForConditionWithoutFailing(() -> !updater.isBusy());
}

View file

@ -72,6 +72,7 @@ public class HelpManager implements HelpService {
private HashMap<URL, HelpSet> urlToHelpSets = new HashMap<>();
private Map<Object, HelpLocation> helpLocations = new WeakHashMap<>();
private Map<Object, DynamicHelpLocation> dynamicHelp = new WeakHashMap<>();
private List<HelpSet> helpSetsPendingMerge = new ArrayList<>();
private boolean hasMergedHelpSets;
@ -137,6 +138,14 @@ public class HelpManager implements HelpService {
return HOME_ID;
}
/**
* Returns the master help set (the one into which all other help sets are merged).
* @return the help set
*/
public GHelpSet getMasterHelpSet() {
return mainHS;
}
@Override
public void excludeFromHelp(Object helpObject) {
excludedFromHelp.add(helpObject);
@ -153,6 +162,11 @@ public class HelpManager implements HelpService {
helpLocations.remove(helpObject);
}
@Override
public void registerDynamicHelp(Object helpObject, DynamicHelpLocation helpLocation) {
dynamicHelp.put(helpObject, helpLocation);
}
@Override
public void registerHelp(Object helpObject, HelpLocation location) {
@ -197,15 +211,29 @@ public class HelpManager implements HelpService {
@Override
public HelpLocation getHelpLocation(Object helpObj) {
return doGetHelpLocation(helpObj);
}
private HelpLocation doGetHelpLocation(Object helpObj) {
DynamicHelpLocation dynamicLocation = dynamicHelp.get(helpObj);
if (dynamicLocation != null) {
HelpLocation hl = dynamicLocation.getActiveHelpLocation();
if (hl != null) {
return hl;
}
}
return helpLocations.get(helpObj);
}
/**
* Returns the master help set (the one into which all other help sets are merged).
* @return the help set
*/
public GHelpSet getMasterHelpSet() {
return mainHS;
private HelpLocation findHelpLocation(Object helpObj) {
if (helpObj instanceof HelpDescriptor) {
HelpDescriptor helpDescriptor = (HelpDescriptor) helpObj;
Object descriptorHelpObj = helpDescriptor.getHelpObject();
return doGetHelpLocation(descriptorHelpObj);
}
return doGetHelpLocation(helpObj);
}
@Override
@ -347,15 +375,6 @@ public class HelpManager implements HelpService {
throw helpException;
}
private HelpLocation findHelpLocation(Object helpObj) {
if (helpObj instanceof HelpDescriptor) {
HelpDescriptor helpDescriptor = (HelpDescriptor) helpObj;
Object helpObject = helpDescriptor.getHelpObject();
return helpLocations.get(helpObject);
}
return helpLocations.get(helpObj);
}
private String getFilenameForHelpLocation(HelpLocation helpLocation) {
URL helpFileURL = getURLForHelpLocation(helpLocation);
if (helpFileURL == null) {

View file

@ -23,108 +23,25 @@ import java.awt.image.VolatileImage;
import generic.theme.GThemeDefaults.Colors.Palette;
import generic.util.image.ImageUtils;
import generic.util.image.ImageUtils.Padding;
import ghidra.util.Msg;
public class Callout {
private static final Color CALLOUT_SHAPE_COLOR = Palette.getColor("palegreen");
private static final Color CALLOUT_SHAPE_COLOR = Palette.getColor("yellowgreen"); //Palette.getColor("palegreen");
private static final int CALLOUT_BORDER_PADDING = 20;
public Image createCallout(CalloutComponentInfo calloutInfo) {
double distanceFactor = 1.15;
//
// Callout Size
//
Dimension cSize = calloutInfo.getSize();
int newHeight = cSize.height * 4;
int calloutHeight = newHeight;
int calloutWidth = calloutHeight; // square
//
// Callout Distance (from original component)
//
double xDistance = calloutWidth * distanceFactor * .80;
double yDistance = calloutHeight * distanceFactor * distanceFactor;
// only pad if the callout leaves the bounds of the parent image
int padding = 0;
Rectangle cBounds = calloutInfo.getBounds();
Point cLoc = cBounds.getLocation();
if (yDistance > cLoc.y) {
// need some padding!
padding = (int) Math.round(calloutHeight * distanceFactor);
cLoc.y += padding;
cBounds.setLocation(cLoc.x, cLoc.y); // move y down by the padding
public Image createCalloutOnImage(Image image, CalloutInfo calloutInfo) {
try {
return doCreateCalloutOnImage(image, calloutInfo);
}
catch (Exception e) {
Msg.error(this, "Unexpected exception creating callout image", e);
throw e;
}
}
boolean goLeft = false;
// TODO for now, always go right
// Rectangle pBounds = parentComponent.getBounds();
// double center = pBounds.getCenterX();
// if (cLoc.x > center) {
// goLeft = true; // callout is on the right of center--go to the left
// }
//
// Callout Bounds
//
int calloutX = (int) (cLoc.x + (goLeft ? -(xDistance + calloutWidth) : xDistance));
int calloutY = (int) (cLoc.y + -yDistance);
int backgroundWidth = calloutWidth;
int backgroundHeight = backgroundWidth; // square
Rectangle calloutBounds =
new Rectangle(calloutX, calloutY, backgroundWidth, backgroundHeight);
//
// Full Callout Shape Bounds
//
Rectangle fullBounds = cBounds.union(calloutBounds);
BufferedImage calloutImage =
createCalloutImage(calloutInfo, cLoc, calloutBounds, fullBounds);
// DropShadow dropShadow = new DropShadow();
// Image shadow = dropShadow.createDrowShadow(calloutImage, 40);
//
// Create our final image and draw into it the callout image and its shadow
//
return calloutImage;
// int width = Math.max(shadow.getWidth(null), calloutImage.getWidth());
// int height = Math.max(shadow.getHeight(null), calloutImage.getHeight());
//
// BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
//
// Graphics g = image.getGraphics();
// Graphics2D g2d = (Graphics2D) g;
// g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
//
// Point imageLoc = calloutInfo.convertPointToParent(fullBounds.getLocation());
// g2d.drawImage(shadow, imageLoc.x, imageLoc.y, null);
// g2d.drawImage(calloutImage, imageLoc.x, imageLoc.y, null);
//
//
//
//
// Debug
//
// g2d.setColor(Palette.RED);
// g2d.draw(fullBounds);
//
// g2d.setColor(Palette.CYAN);
// g2d.draw(calloutBounds);
//
// g2d.setColor(Palette.BLUE);
// g2d.draw(cBounds);
// return image;
}
public Image createCalloutOnImage(Image image, CalloutComponentInfo calloutInfo) {
private Image doCreateCalloutOnImage(Image image, CalloutInfo calloutInfo) {
//
// This code creates a 'call out' image, which is a round, zoomed image of an area
@ -133,134 +50,134 @@ public class Callout {
//
//
// Callout Size
// Callout Size (this is the small image that will be in the center of the overall callout
// shape)
//
Dimension cSize = calloutInfo.getSize();
int newHeight = cSize.height * 6;
Rectangle clientBounds = calloutInfo.getBounds();
Dimension clientShapeSize = clientBounds.getSize();
int newHeight = clientShapeSize.height * 6;
int calloutHeight = newHeight;
int calloutWidth = calloutHeight; // square
//
// Callout Distance (from original component). This is the location (relative to
// the original component) of the callout image (not the full shape). So, if the
// x distance was 10, then the callout image would start 10 pixels to the right of
// the component.
// Callout Offset (from original shape that is being magnified). This is the location
// (relative to the original component) of the callout image (not the full shape; the round
// magnified image). So, if the x offset is 10, then the callout image would start 10 pixels
// to the right of the component.
//
double distanceX = calloutWidth * 1.5;
double distanceY = calloutHeight * 2;
double offsetX = calloutWidth * 1.5;
double offsetY = calloutHeight * 2;
// only pad if the callout leaves the bounds of the parent image
int topPadding = 0;
Rectangle componentBounds = calloutInfo.getBounds();
Point componentLocation = componentBounds.getLocation();
Point imageComponentLocation = calloutInfo.convertPointToParent(componentLocation);
int calloutImageY = imageComponentLocation.y - ((int) distanceY);
if (calloutImageY < 0) {
// the callout would be drawn off the top of the image; pad the image
topPadding = Math.abs(calloutImageY) + CALLOUT_BORDER_PADDING;
// Also, since we have made the image bigger, we have to the component bounds, as
// the callout image uses these bounds to know where to draw the callout. If we
// don't move them, then the padding will cause the callout to be drawn higher
// by the amount of the padding.
componentLocation.y += topPadding;
componentBounds.setLocation(componentLocation.x, componentLocation.y);
}
Point clientLocation = clientBounds.getLocation();
//
// Callout Bounds
//
// angle the callout
// set the callout location offset from the client area and angle it as well
double theta = Math.toRadians(45);
int calloutX = (int) (componentLocation.x + (Math.cos(theta) * distanceX));
int calloutY = (int) (componentLocation.y - (Math.sin(theta) * distanceY));
int backgroundWidth = calloutWidth;
int backgroundHeight = backgroundWidth; // square
Rectangle calloutBounds =
new Rectangle(calloutX, calloutY, backgroundWidth, backgroundHeight);
int calloutX = (int) (clientLocation.x + (Math.cos(theta) * offsetX));
int calloutY = (int) (clientLocation.y - (Math.sin(theta) * offsetY));
Rectangle calloutShapeBounds =
new Rectangle(calloutX, calloutY, calloutWidth, calloutHeight);
//
// Full Callout Shape Bounds (this does not include the drop-shadow)
//
Rectangle calloutDrawingArea = componentBounds.union(calloutBounds);
Rectangle calloutBounds = clientBounds.union(calloutShapeBounds);
BufferedImage calloutImage =
createCalloutImage(calloutInfo, componentLocation, calloutBounds, calloutDrawingArea);
createCalloutImage(calloutInfo, calloutShapeBounds, calloutBounds);
calloutInfo.moveToDestination(calloutBounds);
Point calloutLocation = calloutBounds.getLocation();
int top = calloutLocation.y - CALLOUT_BORDER_PADDING;
if (top < 0) {
// the callout would be drawn off the top of the image; pad the image
topPadding = -top;
}
//
// The drop shadow size is used also to control the offset of the shadow. The shadow is
// twice as big as the callout we will paint. The shadow will be painted first, with the
// callout image on top.
//
DropShadow dropShadow = new DropShadow();
Image shadow = dropShadow.createDropShadow(calloutImage, 40);
//
// Create our final image and draw into it the callout image and its shadow
//
Point calloutImageLoc = calloutInfo.convertPointToParent(calloutDrawingArea.getLocation());
calloutDrawingArea.setLocation(calloutImageLoc);
Rectangle dropShadowBounds = new Rectangle(calloutImageLoc.x, calloutImageLoc.y,
shadow.getWidth(null), shadow.getHeight(null));
Rectangle completeBounds = calloutDrawingArea.union(dropShadowBounds);
int fullBoundsXEndpoint = calloutImageLoc.x + completeBounds.width;
int overlap = fullBoundsXEndpoint - image.getWidth(null);
int rightPadding = 0;
if (overlap > 0) {
rightPadding = overlap + CALLOUT_BORDER_PADDING;
}
int fullBoundsYEndpoint = calloutImageLoc.y + completeBounds.height;
int bottomPadding = 0;
overlap = fullBoundsYEndpoint - image.getHeight(null);
if (overlap > 0) {
bottomPadding = overlap;
}
image =
ImageUtils.padImage(image, Palette.WHITE, topPadding, 0, rightPadding, bottomPadding);
Graphics g = image.getGraphics();
Padding padding = createImagePadding(image, shadow, calloutBounds, topPadding);
Color bg = Palette.WHITE;
Image paddedImage = ImageUtils.padImage(image, bg, padding);
Graphics g = paddedImage.getGraphics();
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.drawImage(shadow, calloutImageLoc.x, calloutImageLoc.y, null);
g2d.drawImage(calloutImage, calloutImageLoc.x, calloutImageLoc.y, null);
// Get the final location that may have been updated if we padded the image
int paddedX = calloutLocation.x += padding.left();
int paddedY = calloutLocation.y += padding.top();
Point finalLocation = new Point(paddedX, paddedY);
g2d.drawImage(shadow, finalLocation.x, finalLocation.y, null);
g2d.drawImage(calloutImage, finalLocation.x, finalLocation.y, null);
//
//
//
//
// Debug
//
// g2d.setColor(Palette.RED);
// g2d.draw(fullBounds);
// Rectangle calloutImageBounds = new Rectangle(finalLocation.x, finalLocation.y,
// calloutImage.getWidth(), calloutImage.getHeight());
// g2d.draw(calloutImageBounds);
//
// g2d.setColor(Palette.CYAN);
// g2d.draw(calloutBounds);
// g2d.setColor(Palette.ORANGE);
// Rectangle destCalloutBounds = new Rectangle(calloutShapeBounds);
// calloutInfo.moveToImage(destCalloutBounds, padding);
// destCalloutBounds.setLocation(destCalloutBounds.getLocation());
// g2d.draw(destCalloutBounds);
//
// g2d.setColor(Palette.BLUE);
// g2d.draw(componentBounds);
//
// g2d.setColor(Palette.MAGENTA);
// g2d.draw(completeBounds);
//
// g2d.setColor(Palette.GRAY);
// g2d.draw(dropShadowBounds);
//
// Point cLocation = componentBounds.getLocation();
// Point convertedCLocation = calloutInfo.convertPointToParent(cLocation);
// g2d.setColor(Palette.PINK);
// componentBounds.setLocation(convertedCLocation);
// g2d.draw(componentBounds);
//
// Point convertedFBLocation = calloutInfo.convertPointToParent(fullBounds.getLocation());
// fullBounds.setLocation(convertedFBLocation);
// g2d.setColor(Palette.ORANGE);
// g2d.draw(fullBounds);
// Rectangle movedClient = new Rectangle(calloutInfo.getBounds());
// calloutInfo.moveToImage(movedClient, padding);
// g2d.draw(movedClient);
return image;
return paddedImage;
}
private BufferedImage createCalloutImage(CalloutComponentInfo calloutInfo, Point cLoc,
Rectangle calloutBounds, Rectangle fullBounds) {
private Padding createImagePadding(Image fullImage, Image shadow, Rectangle calloutOnlyBounds,
int topPad) {
Point calloutLocation = calloutOnlyBounds.getLocation();
int sw = shadow.getWidth(null);
int sh = shadow.getHeight(null);
Rectangle shadowBounds = new Rectangle(calloutLocation.x, calloutLocation.y, sw, sh);
Rectangle combinedBounds = calloutOnlyBounds.union(shadowBounds);
int endX = calloutLocation.x + combinedBounds.width;
int overlap = endX - fullImage.getWidth(null);
int rightPad = 0;
if (overlap > 0) {
rightPad = overlap + CALLOUT_BORDER_PADDING;
}
int endY = calloutLocation.y + combinedBounds.height;
int bottomPad = 0;
overlap = endY - fullImage.getHeight(null);
if (overlap > 0) {
bottomPad = overlap;
}
int leftPad = 0;
return new Padding(topPad, leftPad, rightPad, bottomPad);
}
private BufferedImage createCalloutImage(CalloutInfo calloutInfo,
Rectangle calloutShapeBounds, Rectangle fullBounds) {
//
// The client shape will be to the left of the callout. The client shape and the callout
// bounds together are the full shape.
//
BufferedImage calloutImage =
new BufferedImage(fullBounds.width, fullBounds.height, BufferedImage.TYPE_INT_ARGB);
Graphics2D cg = (Graphics2D) calloutImage.getGraphics();
@ -270,30 +187,33 @@ public class Callout {
// Make relative our two shapes--the component shape and the callout shape
//
Point calloutOrigin = fullBounds.getLocation(); // the shape is relative to the full bounds
int sx = calloutBounds.x - calloutOrigin.x;
int sy = calloutBounds.y - calloutOrigin.y;
Ellipse2D calloutShape =
new Ellipse2D.Double(sx, sy, calloutBounds.width, calloutBounds.height);
int sx = calloutShapeBounds.x - calloutOrigin.x;
int sy = calloutShapeBounds.y - calloutOrigin.y;
int cx = cLoc.x - calloutOrigin.x;
int cy = cLoc.y - calloutOrigin.y;
Dimension cSize = calloutInfo.getSize();
Ellipse2D calloutShape =
new Ellipse2D.Double(sx, sy, calloutShapeBounds.width, calloutShapeBounds.height);
Rectangle clientBounds = calloutInfo.getBounds();
Point clientLocation = clientBounds.getLocation();
int cx = clientLocation.x - calloutOrigin.x;
int cy = clientLocation.y - calloutOrigin.y;
Dimension clientSize = clientBounds.getSize();
// TODO this shows how to correctly account for scaling in the Function Graph
// Dimension cSize2 = new Dimension(cSize);
// double scale = .5d;
// cSize2.width *= scale;
// cSize2.height *= scale;
Rectangle componentShape = new Rectangle(new Point(cx, cy), cSize);
paintCalloutArrow(cg, componentShape, calloutShape);
Rectangle componentShape = new Rectangle(new Point(cx, cy), clientSize);
paintCalloutArrow(cg, componentShape, calloutShape.getBounds());
paintCalloutCircularImage(cg, calloutInfo, calloutShape);
cg.dispose();
return calloutImage;
}
private void paintCalloutCircularImage(Graphics2D g, CalloutComponentInfo calloutInfo,
private void paintCalloutCircularImage(Graphics2D g, CalloutInfo calloutInfo,
RectangularShape shape) {
//
@ -325,8 +245,8 @@ public class Callout {
g.drawImage(foregroundImage, ir.x, ir.y, null);
}
private void paintCalloutArrow(Graphics2D g2d, RectangularShape componentShape,
RectangularShape calloutShape) {
private void paintCalloutArrow(Graphics2D g2d, Rectangle componentShape,
Rectangle calloutShape) {
Rectangle cr = componentShape.getBounds();
Rectangle sr = calloutShape.getBounds();
@ -362,12 +282,10 @@ public class Callout {
}
private Image createMagnifiedImage(GraphicsConfiguration gc, Dimension imageSize,
CalloutComponentInfo calloutInfo, RectangularShape imageShape) {
CalloutInfo calloutInfo, RectangularShape imageShape) {
Dimension componentSize = calloutInfo.getSize();
Point componentScreenLocation = calloutInfo.getLocationOnScreen();
Rectangle r = new Rectangle(componentScreenLocation, componentSize);
Rectangle r = new Rectangle(calloutInfo.getBounds());
calloutInfo.moveToScreen(r);
int offset = 100;
r.x -= offset;
@ -381,7 +299,8 @@ public class Callout {
compImage = robot.createScreenCapture(r);
}
catch (AWTException e) {
throw new RuntimeException("boom", e);
// shouldn't happen
throw new RuntimeException("Unable to create a Robot for capturing the screen", e);
}
double magnification = calloutInfo.getMagnification();

View file

@ -1,99 +0,0 @@
/* ###
* 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 docking.util.image;
import java.awt.*;
import javax.swing.SwingUtilities;
/**
* An object that describes a component to be 'called-out'. A callout is a way to
* emphasize a widget (usually this is only needed for small GUI elements, like an action or
* icon).
*
* <P>The given component info is used to render a magnified image of the given component
* onto another image. For this to work, the rendering engine will need to know how to
* translate the component's location to that of the image space onto which the callout
* will be drawn. This is the purpose of requiring the 'destination component'. That
* component provides the bounds that will be used to move the component's relative position
* (which is relative to the components parent).
*/
public class CalloutComponentInfo {
Point locationOnScreen;
Point relativeLocation;
Dimension size;
Component component;
Component destinationComponent;
double magnification = 2.0;
public CalloutComponentInfo(Component destinationComponent, Component component) {
this(destinationComponent, component, component.getLocationOnScreen(),
component.getLocation(), component.getSize());
}
public CalloutComponentInfo(Component destinationComponent, Component component,
Point locationOnScreen, Point relativeLocation, Dimension size) {
this.destinationComponent = destinationComponent;
this.component = component;
this.locationOnScreen = locationOnScreen;
this.relativeLocation = relativeLocation;
this.size = size;
}
public Point convertPointToParent(Point location) {
return SwingUtilities.convertPoint(component.getParent(), location, destinationComponent);
}
public void setMagnification(double magnification) {
this.magnification = magnification;
}
Component getComponent() {
return component;
}
/**
* Returns the on-screen location of the component. This is used for screen capture, which
* means if you move the component after this info has been created, this location will
* be outdated.
*
* @return the location
*/
Point getLocationOnScreen() {
return locationOnScreen;
}
/**
* The size of the component we will be calling out
*
* @return the size
*/
Dimension getSize() {
return size;
}
Rectangle getBounds() {
return new Rectangle(relativeLocation, size);
}
double getMagnification() {
return magnification;
}
}

View file

@ -0,0 +1,125 @@
/* ###
* 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 docking.util.image;
import java.awt.*;
import javax.swing.SwingUtilities;
import generic.util.image.ImageUtils.Padding;
/**
* An object that describes a component to be 'called-out'. A callout is a way to
* emphasize a widget (usually this is only needed for small GUI elements, like an action or
* icon).
*
* <P>The given component info is used to render a magnified image of the given component
* onto another image. For this to work, the rendering engine will need to know how to
* translate the component's location to that of the image space onto which the callout
* will be drawn. This is the purpose of requiring the 'destination component'. That
* component provides the bounds that will be used to move the component's relative position
* (which is relative to the components parent).
*/
public class CalloutInfo {
private Rectangle clientShape;
private Component source;
private Component destination;
private double magnification = 2.0;
/**
* Constructor for the destination component, the source component and the area that is to be
* captured. This constructor will call out the entire shape of the given source component.
* <p>
* The destination component needs to be the item that was captured in the screenshot. If you
* captured a window, then pass that window as the destination. If you captured a sub-component
* of a window, then pass that sub-component as the destination.
*
* @param destinationComponent the component over which the image will be painted
* @param sourceComponent the component that contains the area that will be called out
*/
public CalloutInfo(Component destinationComponent, Component sourceComponent) {
this(destinationComponent, sourceComponent, sourceComponent.getBounds());
}
/**
* Constructor for the destination component, the source component and the area that is to be
* captured.
* <p>
* The destination component needs to be the item that was captured in the screenshot. If you
* captured a window, then pass that window as the destination. If you captured a sub-component
* of a window, then pass that sub-component as the destination.
*
* @param destinationComponent the component over which the image will be painted
* @param sourceComponent the component that contains the area that will be called out
* @param clientShape the shape that will be called out
*/
public CalloutInfo(Component destinationComponent, Component sourceComponent,
Rectangle clientShape) {
this.destination = destinationComponent;
this.source = sourceComponent;
this.clientShape = clientShape;
}
public void setMagnification(double magnification) {
this.magnification = magnification;
}
public double getMagnification() {
return magnification;
}
/**
* Moves the given rectangle to the image destination space. Clients use this to create new
* shapes using the <B>client space</B> and then move them to the image destination space.
* @param r the rectangle
* @param padding any padding around the destination image
*/
public void moveToImage(Rectangle r, Padding padding) {
moveToDestination(r);
r.x += padding.left();
r.y += padding.top();
}
/**
* Moves the given rectangle to the image destination space. Clients use this to create new
* shapes using the <B>client space</B>. This destination space is not the same as the final
* image that will get created.
* @param r the rectangle
*/
public void moveToDestination(Rectangle r) {
Point oldPoint = r.getLocation();
Point newPoint = SwingUtilities.convertPoint(source.getParent(), oldPoint, destination);
r.setLocation(newPoint);
}
/**
* Moves the given rectangle to screen space. Clients use this to create new shapes using the
* <B>client space</B> and then move them to the image destination space.
* @param r the rectangle
*/
public void moveToScreen(Rectangle r) {
Point p = r.getLocation();
SwingUtilities.convertPointToScreen(p, source.getParent());
r.setLocation(p);
}
public Rectangle getBounds() {
return new Rectangle(clientShape);
}
}

View file

@ -20,8 +20,7 @@ import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.image.*;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.*;
import generic.theme.GThemeDefaults.Colors.Palette;
@ -30,6 +29,103 @@ public class DropShadow {
private Color shadowColor = Palette.BLACK;
private float shadowOpacity = 0.85f;
private void applyShadow(BufferedImage image, int shadowSize) {
int imgWidth = image.getWidth();
int imgHeight = image.getHeight();
int left = (shadowSize - 1) >> 1;
int right = shadowSize - left;
int xStart = left;
int xStop = imgWidth - right;
int yStart = left;
int yStop = imgHeight - right;
int shadowRgb = shadowColor.getRGB() & 0x00ffffff;
int[] aHistory = new int[shadowSize];
int[] data = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
int lastPixelOffset = right * imgWidth;
float sumDivider = shadowOpacity / shadowSize;
// horizontal pass
for (int y = 0, pixel = 0; y < imgHeight; y++, pixel = y * imgWidth) {
int aSum = 0;
int history = 0;
for (int x = 0; x < shadowSize; x++, pixel++) {
int a = data[pixel] >>> 24;
aHistory[x] = a;
aSum += a;
}
pixel -= right;
for (int x = xStart; x < xStop; x++, pixel++) {
int a = (int) (aSum * sumDivider);
data[pixel] = a << 24 | shadowRgb;
// subtract the oldest pixel from the sum
aSum -= aHistory[history];
// get the latest pixel
a = data[pixel + right] >>> 24;
aHistory[history] = a;
aSum += a;
if (++history >= shadowSize) {
history -= shadowSize;
}
}
}
// vertical pass
for (int x = 0, bufferOffset = 0; x < imgWidth; x++, bufferOffset = x) {
int aSum = 0;
int history = 0;
for (int y = 0; y < shadowSize; y++, bufferOffset += imgWidth) {
int a = data[bufferOffset] >>> 24;
aHistory[y] = a;
aSum += a;
}
bufferOffset -= lastPixelOffset;
for (int y = yStart; y < yStop; y++, bufferOffset += imgWidth) {
int a = (int) (aSum * sumDivider);
data[bufferOffset] = a << 24 | shadowRgb;
// subtract the oldest pixel from the sum
aSum -= aHistory[history];
// get the latest pixel
a = data[bufferOffset + lastPixelOffset] >>> 24;
aHistory[history] = a;
aSum += a;
if (++history >= shadowSize) {
history -= shadowSize;
}
}
}
}
private BufferedImage prepareImage(BufferedImage image, int shadowSize) {
int width = image.getWidth() + (shadowSize * 2);
int height = image.getHeight() + (shadowSize * 2);
BufferedImage subject = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = subject.createGraphics();
g2.drawImage(image, null, shadowSize, shadowSize);
g2.dispose();
return subject;
}
public Image createDropShadow(BufferedImage image, int shadowSize) {
BufferedImage subject = prepareImage(image, shadowSize);
applyShadow(subject, shadowSize);
return subject;
}
public static void main(String[] args) {
final DropShadow ds = new DropShadow();
@ -102,148 +198,9 @@ public class DropShadow {
canvas.repaint();
}
});
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setVisible(true);
frame.pack();
}
private void applyShadow(BufferedImage image, int shadowSize) {
int dstWidth = image.getWidth();
int dstHeight = image.getHeight();
int left = (shadowSize - 1) >> 1;
int right = shadowSize - left;
int xStart = left;
int xStop = dstWidth - right;
int yStart = left;
int yStop = dstHeight - right;
int shadowRgb = shadowColor.getRGB() & 0x00ffffff;
int[] aHistory = new int[shadowSize];
int historyIdx = 0;
int aSum;
int[] dataBuffer = ((DataBufferInt) image.getRaster().getDataBuffer()).getData();
int lastPixelOffset = right * dstWidth;
float sumDivider = shadowOpacity / shadowSize;
// horizontal pass
for (int y = 0, bufferOffset = 0; y < dstHeight; y++, bufferOffset = y * dstWidth) {
aSum = 0;
historyIdx = 0;
for (int x = 0; x < shadowSize; x++, bufferOffset++) {
int a = dataBuffer[bufferOffset] >>> 24;
aHistory[x] = a;
aSum += a;
}
bufferOffset -= right;
for (int x = xStart; x < xStop; x++, bufferOffset++) {
int a = (int) (aSum * sumDivider);
dataBuffer[bufferOffset] = a << 24 | shadowRgb;
// subtract the oldest pixel from the sum
aSum -= aHistory[historyIdx];
// get the latest pixel
a = dataBuffer[bufferOffset + right] >>> 24;
aHistory[historyIdx] = a;
aSum += a;
if (++historyIdx >= shadowSize) {
historyIdx -= shadowSize;
}
}
}
// vertical pass
for (int x = 0, bufferOffset = 0; x < dstWidth; x++, bufferOffset = x) {
aSum = 0;
historyIdx = 0;
for (int y = 0; y < shadowSize; y++, bufferOffset += dstWidth) {
int a = dataBuffer[bufferOffset] >>> 24;
aHistory[y] = a;
aSum += a;
}
bufferOffset -= lastPixelOffset;
for (int y = yStart; y < yStop; y++, bufferOffset += dstWidth) {
int a = (int) (aSum * sumDivider);
dataBuffer[bufferOffset] = a << 24 | shadowRgb;
// subtract the oldest pixel from the sum
aSum -= aHistory[historyIdx];
// get the latest pixel
a = dataBuffer[bufferOffset + lastPixelOffset] >>> 24;
aHistory[historyIdx] = a;
aSum += a;
if (++historyIdx >= shadowSize) {
historyIdx -= shadowSize;
}
}
}
}
// private Point computeShadowPosition(double angle, int distance) {
// double angleRadians = Math.toRadians(angle);
// int x = (int) (Math.cos(angleRadians) * distance);
// int y = (int) (Math.sin(angleRadians) * distance);
// return new Point(x, y);
// }
private BufferedImage prepareImage(BufferedImage image, int shadowSize) {
int width = image.getWidth() + (shadowSize * 2);
int height = image.getHeight() + (shadowSize * 2);
BufferedImage subject = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
Graphics2D g2 = subject.createGraphics();
g2.drawImage(image, null, shadowSize, shadowSize);
g2.dispose();
return subject;
}
public Image createDropShadow(BufferedImage image, int shadowSize) {
BufferedImage subject = prepareImage(image, shadowSize);
// BufferedImage shadow =
// new BufferedImage(subject.getWidth(), subject.getHeight(), BufferedImage.TYPE_INT_ARGB);
// BufferedImage shadowMask = createShadowMask(subject);
// getLinearBlueOp(shadowSize).filter(shadowMask, shadow);
applyShadow(subject, shadowSize);
return subject;
}
// private BufferedImage createShadowMask(BufferedImage image) {
//
// BufferedImage mask =
// new BufferedImage(image.getWidth(), image.getHeight(), BufferedImage.TYPE_INT_ARGB);
//
// Graphics2D g2 = mask.createGraphics();
// g2.drawImage(image, 0, 0, null);
// g2.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_IN, shadowOpacity));
//
// g2.setColor(shadowColor);
//
// g2.fillRect(0, 0, image.getWidth(), image.getHeight());
// g2.dispose();
//
// return mask;
// }
//
// private ConvolveOp getLinearBlueOp(int size) {
// float[] data = new float[size * size];
// float value = 1.0f / (size * size);
// for (int i = 0; i < data.length; i++) {
// data[i] = value;
// }
// return new ConvolveOp(new Kernel(size, size, data));
// }
}

View file

@ -16,9 +16,14 @@
package docking.widgets;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.help.UnsupportedOperationException;
import javax.swing.ListCellRenderer;
import org.apache.commons.lang3.StringUtils;
import docking.widgets.list.GListCellRenderer;
import ghidra.util.datastruct.CaseInsensitiveDuplicateStringComparator;
@ -53,8 +58,48 @@ public class DefaultDropDownSelectionDataModel<T> implements DropDownTextFieldDa
Collections.sort(data, comparator);
}
@Override
public List<SearchMode> getSupportedSearchModes() {
return List.of(SearchMode.STARTS_WITH, SearchMode.CONTAINS, SearchMode.WILDCARD);
}
@Override
public List<T> getMatchingData(String searchText) {
throw new UnsupportedOperationException(
"Method no longer supported. Instead, call getMatchingData(String, SearchMode)");
}
@Override
public List<T> getMatchingData(String searchText, SearchMode mode) {
if (StringUtils.isBlank(searchText)) {
return new ArrayList<>(data);
}
if (!getSupportedSearchModes().contains(mode)) {
throw new IllegalArgumentException("Unsupported SearchMode: " + mode);
}
if (mode == SearchMode.STARTS_WITH) {
return getMatchingDataStartsWith(searchText);
}
Pattern p = mode.createPattern(searchText);
return getMatchingDataRegex(p);
}
private List<T> getMatchingDataRegex(Pattern p) {
List<T> results = new ArrayList<>();
for (T t : data) {
String string = searchConverter.getString(t);
Matcher m = p.matcher(string);
if (m.matches()) {
results.add(t);
}
}
return results;
}
private List<T> getMatchingDataStartsWith(String searchText) {
List<?> l = data;
int startIndex = Collections.binarySearch(l, (Object) searchText, comparator);
int endIndex = Collections.binarySearch(l, (Object) (searchText + END_CHAR), comparator);

View file

@ -17,25 +17,35 @@ package docking.widgets;
import java.awt.*;
import java.awt.event.*;
import java.awt.font.FontRenderContext;
import java.awt.font.GlyphVector;
import java.awt.geom.Rectangle2D;
import java.util.*;
import java.util.List;
import javax.swing.*;
import javax.swing.border.BevelBorder;
import javax.swing.event.*;
import javax.swing.text.Caret;
import org.apache.commons.lang3.StringUtils;
import docking.DockingWindowManager;
import docking.widgets.DropDownTextFieldDataModel.SearchMode;
import docking.widgets.label.GDHtmlLabel;
import docking.widgets.list.GList;
import generic.theme.GColor;
import generic.theme.GThemeDefaults.Colors;
import generic.theme.GThemeDefaults.Colors.Messages;
import generic.theme.GThemeDefaults.Colors.Tooltips;
import generic.util.WindowUtilities;
import ghidra.util.StringUtilities;
import ghidra.util.SystemUtilities;
import ghidra.framework.options.PreferenceState;
import ghidra.util.*;
import ghidra.util.datastruct.WeakDataStructureFactory;
import ghidra.util.datastruct.WeakSet;
import ghidra.util.task.SwingUpdateManager;
import help.Help;
import help.HelpService;
import util.CollectionUtils;
/**
@ -60,6 +70,8 @@ import util.CollectionUtils;
*/
public class DropDownTextField<T> extends JTextField implements GComponent {
private static final Cursor CURSOR_HAND = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
private static final Cursor CURSOR_DEFAULT = Cursor.getDefaultCursor();
private static final int DEFAULT_MAX_UPDATE_DELAY = 2000;
private static final int MIN_HEIGHT = 300;
private static final int MIN_WIDTH = 200;
@ -90,7 +102,7 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
protected boolean internallyDrivenUpdate;
private boolean consumeEnterKeyPress = true; // consume Enter presses by default
private boolean ignoreEnterKeyPress = false; // do not ignore enter by default
private boolean ignoreCaretChanges;
private boolean textFieldNotFocused;
private boolean showMachingListOnEmptyText;
// We use an update manager to buffer requests to update the matches. This allows us to be
@ -106,6 +118,16 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
*/
private String currentMatchingText;
/**
* Search mode support. Clients specify search modes that allow the user to change how results
* are matched. For backward compatibility, this will be empty for clients that have not
* specified search modes.
*/
private List<SearchMode> searchModes = new ArrayList<>();
private boolean searchModeIsHovered;
private SearchMode searchMode = SearchMode.UNKNOWN;
private SearchModeBounds searchModeBounds;
/**
* Constructor.
* <p>
@ -132,7 +154,36 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
init(updateMinDelay);
}
@Override
public void updateUI() {
// reset the hint bounds; this value is based on the current font
searchModeBounds = null;
super.updateUI();
}
private void init(int updateMinDelay) {
List<SearchMode> modes = dataModel.getSupportedSearchModes();
for (SearchMode mode : modes) {
if (mode != SearchMode.UNKNOWN && !searchModes.contains(mode)) {
searchModes.add(mode);
// pick the first mode to use
if (searchMode == SearchMode.UNKNOWN) {
searchMode = mode;
}
}
}
installSearchModeDisplay();
// add a one-time listener to this field to restore any saved state, like the search mode
DockingWindowManager.registerComponentLoadedListener(this, (dwm, provider) -> {
loadPreferenceState();
});
updateManager = new SwingUpdateManager(updateMinDelay, DEFAULT_MAX_UPDATE_DELAY,
"Drop Down Selection Text Field Update Manager", () -> {
if (pendingTextUpdate == null) {
@ -151,6 +202,121 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
initDataList();
getAccessibleContext().setAccessibleName("Data Type Editor");
HelpService help = Help.getHelpService();
help.registerDynamicHelp(this, new SearchModeHelpLocation());
}
private void installSearchModeDisplay() {
if (!hasMultipleSearchModes()) {
return;
}
addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
// when resized, update the location of the search mode hint when we get repainted
searchModeBounds = null;
}
});
SearchModeMouseListener mouseListener = new SearchModeMouseListener();
addMouseMotionListener(mouseListener);
addMouseListener(mouseListener);
}
private boolean hasMultipleSearchModes() {
return searchModes.size() > 1;
}
private boolean isOverSearchMode(MouseEvent e) {
if (searchModeBounds == null) {
return false; // have not yet been painted
}
Point p = e.getPoint();
return searchModeBounds.isHovered(p);
}
public SearchMode getSearchMode() {
return searchMode;
}
public void setSearchMode(SearchMode newMode) {
if (!searchModes.contains(newMode)) {
throw new IllegalArgumentException(
"Search mode is not supported by this texts field: " + newMode);
}
doSetSearchMode(newMode);
}
private void doSetSearchMode(SearchMode newMode) {
searchMode = newMode;
searchModeBounds = null;
repaint();
savePreferenceState();
maybeUpdateDisplayContents(true);
}
private void toggleSearchMode(boolean forward) {
if (!hasMultipleSearchModes()) {
return;
}
int index = searchModes.indexOf(searchMode);
int next = forward ? index + 1 : index - 1;
if (forward) {
if (next == searchModes.size()) {
next = 0;
}
}
else {
if (next == -1) {
next = searchModes.size() - 1;
}
}
SearchMode newMode = searchModes.get(next);
doSetSearchMode(newMode);
}
private void savePreferenceState() {
String preferenceKey = dataModel.getClass().getSimpleName();
PreferenceState state = new PreferenceState();
state.putEnum("searchMode", searchMode);
// We are in the UI at this point, so we have a valid window manager. (The window manager
// may be null in testing.)
DockingWindowManager dwm = DockingWindowManager.getInstance(this);
if (dwm != null) {
dwm.putPreferenceState(preferenceKey, state);
}
}
private void loadPreferenceState() {
String preferenceKey = dataModel.getClass().getSimpleName();
// We are in the UI at this point, so we have a valid window manager. (The window manager
// may be null in testing.)
DockingWindowManager dwm = DockingWindowManager.getInstance(this);
if (dwm == null) {
return;
}
PreferenceState state = dwm.getPreferenceState(preferenceKey);
if (state == null) {
return;
}
searchMode = state.getEnum("searchMode", searchMode);
searchModeBounds = null;
repaint();
}
protected ListSelectionModel createListSelectionModel() {
@ -300,11 +466,44 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
updateManager.updateLater();
}
private void maybeUpdateDisplayContents(String userText) {
if (SystemUtilities.isEqual(userText, pendingTextUpdate)) {
private void maybeUpdateDisplayContents(boolean force) {
if (textFieldNotFocused) {
return;
}
updateDisplayContents(userText);
String text = getText();
if (StringUtils.isBlank(text)) {
return;
}
// caret position only matters with 'starts with', as the user can arrow through the text
// to change which text the 'starts with' matches
if (!isStartsWithSearch()) {
if (force || isDifferentText(text)) {
updateDisplayContents(text);
}
return;
}
Caret caret = getCaret();
int dot = caret.getDot();
String textToCaret = text.substring(0, dot);
if (force || isDifferentText(textToCaret)) {
updateDisplayContents(textToCaret);
}
}
private boolean isDifferentText(String newText) {
return !CollectionUtils.isOneOf(newText, currentMatchingText, pendingTextUpdate);
}
private boolean isStartsWithSearch() {
if (hasMultipleSearchModes()) {
return searchMode == SearchMode.STARTS_WITH;
}
return searchMode == SearchMode.STARTS_WITH ||
searchMode == SearchMode.UNKNOWN; // backward compatibility
}
private void doUpdateDisplayContents(String userText) {
@ -367,8 +566,14 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
Cursor previousCursor = getCursor();
try {
setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
if (searchMode == SearchMode.UNKNOWN) {
// backward compatible
return dataModel.getMatchingData(searchText);
}
return dataModel.getMatchingData(searchText, searchMode);
}
finally {
setCursor(previousCursor);
}
@ -383,7 +588,8 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
/**
* Shows the matching list. This can be used to show all data when the user has not typed any
* text.
* text. For data models that have large data sets, this call may not show the matching list.
* This behavior is determine by the current data model.
*/
public void showMatchingList() {
@ -702,6 +908,66 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
windowVisibilityListener = Objects.requireNonNull(l);
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
if (searchMode == SearchMode.UNKNOWN) {
return;
}
String modeHint = searchMode.getHint();
searchModeBounds = calculateSearchModeBounds(modeHint, g);
Color textColor = searchModeIsHovered ? Colors.FOREGROUND : Messages.HINT;
Graphics2D g2 = (Graphics2D) g;
g2.setColor(textColor);
g2.setFont(g2.getFont().deriveFont(Font.ITALIC));
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
Dimension size = getSize();
Insets insets = getInsets();
int bottomPad = 3;
int x = searchModeBounds.getTextStartX();
int y = size.height - (insets.bottom + bottomPad); // strings paint bottom-up
g2.drawString(modeHint, x, y);
// debug
// g.setColor(Color.ORANGE);
// g2.draw(searchModeBounds.hoverAreaBounds);
}
private SearchModeBounds calculateSearchModeBounds(String text, Graphics g) {
if (searchModeBounds != null) {
return searchModeBounds;
}
Graphics2D g2d = (Graphics2D) g;
Font f = g.getFont();
FontRenderContext frc = g2d.getFontRenderContext();
char[] chars = text.toCharArray();
int n = text.length();
GlyphVector gv = f.layoutGlyphVector(frc, chars, 0, n, Font.LAYOUT_LEFT_TO_RIGHT);
Rectangle2D bounds2d = gv.getVisualBounds();
searchModeBounds = new SearchModeBounds(bounds2d.getBounds());
return searchModeBounds;
}
/**
* Returns the search mode bounds. This is the area of the text field that shows the current
* search mode. This area can be hovered and clicked by the user. If there are not multiple
* search modes available, then this area is not painted and the bounds will be null. This
* value will get updated as this text field is resized.
*
* @return the search mode bounds
*/
public SearchModeBounds getSearchModeBounds() {
return searchModeBounds;
}
//=================================================================================================
// Inner Classes
//=================================================================================================
@ -738,13 +1004,13 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
return;
}
ignoreCaretChanges = true;
textFieldNotFocused = true;
hideMatchingWindow();
}
@Override
public void focusGained(FocusEvent e) {
ignoreCaretChanges = false;
textFieldNotFocused = false;
}
}
@ -777,21 +1043,7 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
private class UpdateCaretListener implements CaretListener {
@Override
public void caretUpdate(CaretEvent event) {
if (ignoreCaretChanges) {
return;
}
String text = getText();
if (text == null || text.isEmpty()) {
return;
}
String textToCaret = text.substring(0, event.getDot());
if (textToCaret.equals(currentMatchingText)) {
return; // nothing to do
}
maybeUpdateDisplayContents(textToCaret);
maybeUpdateDisplayContents(false);
}
}
@ -910,12 +1162,25 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
private void handleArrowKey(KeyEvent event) {
if (getMatchingWindow().isShowing()) {
handleArrowKeyForMatchingWindow(event);
return;
}
// Contrl-Up/Down is for toggling the search mode
if (event.isControlDown()) {
int keyCode = event.getKeyCode();
if (!getMatchingWindow().isShowing()) {
boolean forward = keyCode == KeyEvent.VK_DOWN || keyCode == KeyEvent.VK_KP_DOWN;
toggleSearchMode(forward);
return;
}
updateDisplayContents(getText());
event.consume();
}
else { // update the window if it is showing
private void handleArrowKeyForMatchingWindow(KeyEvent event) {
int keyCode = event.getKeyCode();
if (keyCode == KeyEvent.VK_UP || keyCode == KeyEvent.VK_KP_UP) {
decrementListSelection();
}
@ -925,7 +1190,6 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
event.consume();
setTextFromSelectedListItemAndKeepMatchingWindowOpen();
}
}
private void incrementListSelection() {
int index = list.getSelectedIndex();
@ -1038,6 +1302,108 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
public void setLeadSelectionIndex(int leadIndex) {
// stub
}
}
private class SearchModeMouseListener extends MouseAdapter {
@Override
public void mouseClicked(MouseEvent e) {
if (e.getClickCount() != 1) {
return;
}
if (!isOverSearchMode(e)) {
return;
}
boolean forward = !e.isControlDown();
toggleSearchMode(forward);
}
private void updateSearchModeHover(MouseEvent e) {
searchModeIsHovered = isOverSearchMode(e);
String tip =
searchModeIsHovered ? "Search Mode: " + searchMode.getDisplayName() : null;
setToolTipText(tip);
setCursor(searchModeIsHovered ? CURSOR_HAND : CURSOR_DEFAULT);
repaint();
}
@Override
public void mouseMoved(MouseEvent e) {
updateSearchModeHover(e);
}
@Override
public void mouseEntered(MouseEvent e) {
updateSearchModeHover(e);
}
@Override
public void mouseExited(MouseEvent e) {
updateSearchModeHover(e);
}
}
private class SearchModeHelpLocation implements DynamicHelpLocation {
// Note the help for this generic field currently lives in the help for the Data Type
// chooser, which is a bit odd, but convenient. To fix this, we would need a separate help
// page for the generic text field.
private HelpLocation helpLocation = new HelpLocation("DataTypeEditors", "SearchMode");
@Override
public HelpLocation getActiveHelpLocation() {
if (searchModeIsHovered) {
return helpLocation;
}
return null;
}
}
/**
* Represents the bounds of the search mode area in this text field. This also tracks the text
* position within the search mode bounds.
*/
public class SearchModeBounds {
private Rectangle textBounds;
private Rectangle hoverAreaBounds;
SearchModeBounds(Rectangle textBounds) {
this.textBounds = textBounds;
Dimension size = getSize();
Insets insets = getInsets();
hoverAreaBounds = new Rectangle(textBounds);
hoverAreaBounds.width += 10; // add some padding
// same height as this field
hoverAreaBounds.height = getHeight() - (insets.top + insets.bottom);
// move away from the end of this field
hoverAreaBounds.x = size.width - insets.right - hoverAreaBounds.width;
hoverAreaBounds.y = insets.top;
}
public Rectangle getHoverAreaBounds() {
return hoverAreaBounds;
}
boolean isHovered(Point p) {
return hoverAreaBounds.contains(p);
}
Point getLocation() {
return hoverAreaBounds.getLocation();
}
int getTextWidth() {
return textBounds.width;
}
int getTextStartX() {
return (int) hoverAreaBounds.getCenterX() - (getTextWidth() / 2);
}
}
}

View file

@ -15,10 +15,16 @@
*/
package docking.widgets;
import java.util.List;
import static ghidra.util.UserSearchUtils.*;
import java.util.List;
import java.util.regex.Pattern;
import javax.help.UnsupportedOperationException;
import javax.swing.ListCellRenderer;
import ghidra.util.UserSearchUtils;
/**
* This interface represents all methods needed by the {@link DropDownSelectionTextField} in order
* to search, show, manipulate and select objects.
@ -27,15 +33,112 @@ import javax.swing.ListCellRenderer;
*/
public interface DropDownTextFieldDataModel<T> {
public enum SearchMode {
/** Matches when any line of data contains the search text */
CONTAINS("()", "Contains"),
/** Matches when any line of data starts with the search text */
STARTS_WITH("^", "Starts With"),
/** Matches when any line of data contains the search text using globbing characters */
WILDCARD("*?", "Wildcard"),
/** Used internally */
UNKNOWN("", "");
private String hint;
private String displayName;
SearchMode(String hint, String displayName) {
this.hint = hint;
this.displayName = displayName;
}
public String getHint() {
return hint;
}
public String getDisplayName() {
return displayName;
}
/**
* Returns a list of data that matches the given <code>searchText</code>. A match typically
* means a "startsWith" match. A list is returned to allow for multiple matches.
* Creates search pattern for the given input text. Clients do not have to use this method
* and a free to create their own text matching mechanism.
* @param input the input for which to search
* @return the pattern
* @see UserSearchUtils
*/
public Pattern createPattern(String input) {
switch (this) {
case CONTAINS:
return createContainsPattern(input, false, Pattern.CASE_INSENSITIVE);
case STARTS_WITH:
return createStartsWithPattern(input, false, Pattern.CASE_INSENSITIVE);
case WILDCARD:
return createSearchPattern(input, false);
default:
throw new IllegalStateException("Cannot create pattern for mode: " + this);
}
}
}
/**
* Returns a list of data that matches the given <code>searchText</code>. A list is returned to
* allow for multiple matches. The type of matching performed is determined by the current
* {@link #getSupportedSearchModes() search mode}. If the implementation of this model does not
* support search modes, then it is up the the implementor to determine how matches are found.
* <P>
* Implementation Note: a client request for all data will happen using the empty string. If
* your data model is sufficiently large, then you may choose to not return any data in this
* case. Smaller data sets should return all data when given the empty string
*
* @param searchText The text used to find matches.
* @return a list of items matching the given text.
* @see #getMatchingData(String, SearchMode)
*/
public List<T> getMatchingData(String searchText);
/**
* Returns a list of data that matches the given <code>searchText</code>. A list is returned to
* allow for multiple matches. The type of matching performed is determined by the current
* {@link #getSupportedSearchModes() search mode}. If the implementation of this model does not
* support search modes, then it is up the the implementor to determine how matches are found.
* <P>
* Implementation Note: a client request for all data will happen using the empty string. If
* your data model is sufficiently large, then you may choose to not return any data in this
* case. Smaller data sets should return all data when given the empty string
*
* @param searchText the text used to find matches.
* @param searchMode the search mode to use
* @return a list of items matching the given text.
* @throws IllegalArgumentException if the given search mode is not supported
* @see #getMatchingData(String, SearchMode)
*/
public default List<T> getMatchingData(String searchText, SearchMode searchMode) {
// Clients that override getSupportedSearchModes() must also override this method to perform
// the correct type of search
if (searchMode != SearchMode.UNKNOWN) {
throw new UnsupportedOperationException(
"You must override this method to use search modes");
}
// Use the default matching data
return getMatchingData(searchText);
}
/**
* Subclasses can override this to return all supported search modes. The order of the modes is
* the order which they will cycle when requested by the user. The first mode is the default
* search mode.
* @return the supported search modes
*/
public default List<SearchMode> getSupportedSearchModes() {
return List.of(SearchMode.UNKNOWN);
}
/**
* Returns the index in the given list of the first item that matches the given text. For
* data sets that do not allow duplicates, this is simply the index of the item that matches

View file

@ -18,10 +18,15 @@ package docking.widgets.filechooser;
import java.awt.Component;
import java.io.File;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.help.UnsupportedOperationException;
import javax.swing.*;
import javax.swing.filechooser.FileSystemView;
import org.apache.commons.lang3.StringUtils;
import docking.widgets.DropDownSelectionTextField;
import docking.widgets.DropDownTextFieldDataModel;
import docking.widgets.list.GListCellRenderer;
@ -84,12 +89,58 @@ public class FileDropDownSelectionDataModel implements DropDownTextFieldDataMode
return new FileDropDownRenderer();
}
@Override
public List<SearchMode> getSupportedSearchModes() {
return List.of(SearchMode.STARTS_WITH, SearchMode.CONTAINS, SearchMode.WILDCARD);
}
@Override
public List<File> getMatchingData(String searchText) {
if (searchText == null || searchText.length() == 0) {
throw new UnsupportedOperationException(
"Method no longer supported. Instead, call getMatchingData(String, SearchMode)");
}
@Override
public List<File> getMatchingData(String searchText, SearchMode mode) {
if (StringUtils.isBlank(searchText)) {
// full data display not support, as we don't know how big the data may be
return Collections.emptyList();
}
if (!getSupportedSearchModes().contains(mode)) {
throw new IllegalArgumentException("Unsupported SearchMode: " + mode);
}
if (mode == SearchMode.STARTS_WITH) {
return getMatchDataStartsWith(searchText);
}
Pattern p = mode.createPattern(searchText);
return getMatchingDataRegex(p);
}
private List<File> getMatchingDataRegex(Pattern p) {
List<File> matches = new ArrayList<>();
List<File> list = getSortedFiles();
for (File file : list) {
String name = file.getName();
Matcher m = p.matcher(name);
if (m.matches()) {
matches.add(file);
}
}
return matches;
}
private List<File> getMatchDataStartsWith(String searchText) {
List<File> list = getSortedFiles();
return getMatchingSubList(searchText, searchText + END_CHAR, list);
}
private List<File> getSortedFiles() {
File directory = chooser.getCurrentDirectory();
File[] files = directory.listFiles();
if (files == null) {
@ -101,8 +152,7 @@ public class FileDropDownSelectionDataModel implements DropDownTextFieldDataMode
}
Collections.sort(list, sortComparator);
return getMatchingSubList(searchText, searchText + END_CHAR, list);
return list;
}
private List<File> getMatchingSubList(String searchTextStart, String searchTextEnd,

View file

@ -138,9 +138,15 @@ public class AutocompletingStringConstraintEditor extends DataLoadingConstraintE
@Override
public List<String> getMatchingData(String searchText) {
if (StringUtils.isBlank(searchText) || !isValidPatternString(searchText)) {
if (!isValidPatternString(searchText)) {
return Collections.emptyList();
}
if (StringUtils.isBlank(searchText)) {
// full data display not supported, as we don't know how big the data may be
return Collections.emptyList();
}
searchText = searchText.trim();
lastConstraint = (StringColumnConstraint) currentConstraint
.parseConstraintValue(searchText, columnDataSource.getTableDataSource());

View file

@ -19,8 +19,7 @@ import static org.junit.Assert.*;
import java.awt.BorderLayout;
import java.awt.event.*;
import java.util.Arrays;
import java.util.List;
import java.util.*;
import javax.swing.*;
import javax.swing.event.CellEditorListener;
@ -30,6 +29,7 @@ import org.junit.After;
import org.junit.Before;
import docking.test.AbstractDockingTest;
import docking.widgets.DropDownTextFieldDataModel.SearchMode;
public abstract class AbstractDropDownTextFieldTest<T> extends AbstractDockingTest {
@ -151,21 +151,30 @@ public abstract class AbstractDropDownTextFieldTest<T> extends AbstractDockingTe
return item;
}
/** The item that is selected in the JList; not the 'selectedValue' in the text field */
/**
* The item that is selected in the JList; not the 'selectedValue' in the text field
* @param expected the expected value
*/
protected void assertSelectedListItem(int expected) {
JList<T> list = textField.getJList();
int actual = runSwing(() -> list.getSelectedIndex());
assertEquals(expected, actual);
}
/** The item that is selected in the JList; not the 'selectedValue' in the text field */
/**
* The item that is selected in the JList; not the 'selectedValue' in the text field
* @param expected the expected items
*/
protected void assertSelectedListItem(T expected) {
JList<T> list = textField.getJList();
T actual = runSwing(() -> list.getSelectedValue());
assertEquals(expected, actual);
}
/** The 'selectedValue' made after the user makes a choice */
/**
* The 'selectedValue' made after the user makes a choice
* @param expected the expected value
*/
protected void assertSelectedValue(T expected) {
T actual = runSwing(() -> textField.getSelectedValue());
assertEquals(expected, actual);
@ -177,6 +186,24 @@ public abstract class AbstractDropDownTextFieldTest<T> extends AbstractDockingTe
assertNull(actual);
}
protected void assertMatchesInList(String... expected) {
waitForSwing();
assertMatchingWindowShowing();
@SuppressWarnings("unchecked")
JList<String> list = (JList<String>) textField.getJList();
ListModel<String> model = list.getModel();
int n = model.getSize();
assertEquals("Expected item size is not the same as the matching list size",
expected.length, n);
HashSet<String> set = new HashSet<>(Arrays.asList(expected));
for (int i = 0; i < n; i++) {
String item = model.getElementAt(i);
assertTrue("Item in list not expected: " + item, set.contains(item));
}
}
protected void assertNoEditingCancelledEvent() {
assertEquals("Received unexpected editingCanceled() invocations.", listener.canceledCount,
0);
@ -252,6 +279,15 @@ public abstract class AbstractDropDownTextFieldTest<T> extends AbstractDockingTe
runSwing(() -> textField.setText(text));
}
protected void setSearchMode(SearchMode newMode) {
runSwing(() -> textField.setSearchMode(newMode));
}
protected void assertSearchMode(SearchMode expected) {
SearchMode actual = runSwing(() -> textField.getSearchMode());
assertEquals(expected, actual);
}
protected void closeMatchingWindow() {
JWindow window = runSwing(() -> textField.getActiveMatchingWindow());
if (window == null) {
@ -294,6 +330,16 @@ public abstract class AbstractDropDownTextFieldTest<T> extends AbstractDockingTe
waitForSwing();
}
protected void left() {
tpyeActionKey(KeyEvent.VK_LEFT);
waitForSwing();
}
protected void right() {
tpyeActionKey(KeyEvent.VK_RIGHT);
waitForSwing();
}
protected void typeText(final String text, boolean expectWindow) {
waitForSwing();
triggerText(textField, text);

View file

@ -15,7 +15,7 @@
*/
package docking.widgets;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.*;
import java.util.ArrayList;
import java.util.List;
@ -23,6 +23,7 @@ import java.util.List;
import org.junit.Before;
import org.junit.Test;
import docking.widgets.DropDownTextFieldDataModel.SearchMode;
import generic.test.AbstractGenericTest;
public class DefaultDropDownSelectionDataModelTest extends AbstractGenericTest {
@ -48,11 +49,11 @@ public class DefaultDropDownSelectionDataModelTest extends AbstractGenericTest {
@Test
public void testGetMatchingData() {
List<TestType> matchingData = model.getMatchingData("a");
List<TestType> matchingData = model.getMatchingData("a", SearchMode.STARTS_WITH);
assertEquals(1, matchingData.size());
assertEquals("abc", matchingData.get(0).getName());
matchingData = model.getMatchingData("bac");
matchingData = model.getMatchingData("bac", SearchMode.STARTS_WITH);
assertEquals(2, matchingData.size());
assertEquals("bac", matchingData.get(0).getName());
assertEquals("bace", matchingData.get(1).getName());

View file

@ -19,14 +19,15 @@ import static org.junit.Assert.*;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.awt.event.*;
import javax.swing.JList;
import javax.swing.JWindow;
import org.junit.Test;
import docking.widgets.DropDownTextFieldDataModel.SearchMode;
/**
* This test achieves partial coverage of {@link DropDownTextField}. Further coverage is
* provided by {@link DropDownSelectionTextFieldTest}, as that test enables item selection
@ -212,10 +213,12 @@ public class DropDownTextFieldTest extends AbstractDropDownTextFieldTest<String>
runSwing(() -> parentFrame.setLocation(p));
waitForSwing();
// we expect the location to change, but there may be a delay
waitForCondition(() -> {
JWindow currentMatchingWindow = textField.getActiveMatchingWindow();
Point newLocation = runSwing(() -> currentMatchingWindow.getLocationOnScreen());
assertNotEquals("The completion window's location did not update when its parent window " +
"was moved.", location, newLocation);
return !location.equals(newLocation);
});
}
@Test
@ -470,6 +473,190 @@ public class DropDownTextFieldTest extends AbstractDropDownTextFieldTest<String>
assertMatchingWindowShowing();
}
@Test
public void testSearchMode_Contains() {
setSearchMode(SearchMode.CONTAINS);
typeText("1", true);
assertMatchesInList("a1", "d1", "e1", "e12", "e123");
clearText();
typeText("e1", true);
assertMatchesInList("e1", "e12", "e123");
clearText();
typeText("z", false);
assertMatchingWindowHidden();
}
@Test
public void testSearchMode_Contains_CaretPositionDoesNotAffectResults() {
setSearchMode(SearchMode.CONTAINS);
typeText("e12", true);
assertMatchesInList("e12", "e123");
left(); // move caret back one position: from e12| to e1|2
assertMatchesInList("e12", "e123");
left(); // move caret back one position: from e1|2 to e|12
assertMatchesInList("e12", "e123");
right(); // move caret back to e1|2
assertMatchesInList("e12", "e123");
right(); // move caret back to e12|
assertMatchesInList("e12", "e123");
}
@Test
public void testSearchMode_StartsWith_CaretPositionChangesResults() {
setSearchMode(SearchMode.STARTS_WITH);
typeText("e12", true);
assertMatchesInList("e12", "e123");
left(); // move caret back one position: from e12| to e1|2
assertMatchesInList("e1", "e12", "e123");
right(); // move caret back to e12|
assertMatchesInList("e12", "e123");
}
@Test
public void testSearchMode_ChangeModeWithText_ToStartsWith_CaretPositionChangesResults() {
/*
The text field honors caret position in 'starts with' mode. Test that changing modes
with text in the field will correctly use the caret position for the given mode.
*/
// start with a search mode that ignores the caret position
setSearchMode(SearchMode.CONTAINS);
typeText("e12", true);
assertMatchesInList("e12", "e123");
left(); // move caret back one position: from e12| to e1|2
assertMatchesInList("e12", "e123"); // same matches in 'contains' mode
setSearchMode(SearchMode.STARTS_WITH);
assertMatchesInList("e1", "e12", "e123"); // caret is at e1|2; matches should change
setSearchMode(SearchMode.CONTAINS);
assertMatchesInList("e12", "e123"); // matches now ignore the caret
}
@Test
public void testSearchMode_Contains_CaretPositionDoesNotChangesResults() {
setSearchMode(SearchMode.CONTAINS);
typeText("e12", true);
assertMatchesInList("e12", "e123");
left(); // move caret back one position: from e12| to e1|2
assertMatchesInList("e12", "e123");
right(); // move caret back to e12|
assertMatchesInList("e12", "e123");
}
@Test
public void testChangeSearchMode_ViaKeyBinding() {
/*
Default search mode order:
STARTS_WITH, CONTAINS, WILDCARD
*/
assertSearchMode(SearchMode.STARTS_WITH);
toggleSearchModeViaKeyBinding();
assertSearchMode(SearchMode.CONTAINS);
toggleSearchModeViaKeyBinding();
assertSearchMode(SearchMode.WILDCARD);
toggleSearchModeViaKeyBinding();
assertSearchMode(SearchMode.STARTS_WITH);
toggleSearchModeViaKeyBinding_Backwards();
assertSearchMode(SearchMode.WILDCARD);
toggleSearchModeViaKeyBinding_Backwards();
assertSearchMode(SearchMode.CONTAINS);
}
@Test
public void testChangeSearchMode_ViaMouse() {
/*
Default search mode order:
STARTS_WITH, CONTAINS, WILDCARD
*/
assertSearchMode(SearchMode.STARTS_WITH);
toggleSearchModeViaMouseClick();
assertSearchMode(SearchMode.CONTAINS);
toggleSearchModeViaMouseClick();
assertSearchMode(SearchMode.WILDCARD);
toggleSearchModeViaMouseClick();
assertSearchMode(SearchMode.STARTS_WITH);
toggleSearchModeViaMouseClick_Backwards();
assertSearchMode(SearchMode.WILDCARD);
toggleSearchModeViaMouseClick_Backwards();
assertSearchMode(SearchMode.CONTAINS);
}
private void toggleSearchModeViaMouseClick() {
clickSearchMode(false);
}
private void toggleSearchModeViaMouseClick_Backwards() {
clickSearchMode(true);
}
private void clickSearchMode(boolean useControlKey) {
// we have to wait, since the bounds are set when the text field paints
DropDownTextField<String>.SearchModeBounds searchModeBounds = waitFor(() -> {
return runSwing(() -> textField.getSearchModeBounds());
});
// this point is relative to the text field
Point p = searchModeBounds.getLocation();
long when = System.currentTimeMillis();
int mods = useControlKey ? InputEvent.CTRL_DOWN_MASK : 0;
int x = p.x + 3; // add some fudge
int y = p.y + 3; // add some fudge
int clickCount = 1;
boolean popupTrigger = false;
MouseEvent event = new MouseEvent(textField, MouseEvent.MOUSE_CLICKED, when, mods, x, y,
clickCount, popupTrigger);
runSwing(() -> textField.dispatchEvent(event));
}
private void toggleSearchModeViaKeyBinding() {
triggerKey(textField, InputEvent.CTRL_DOWN_MASK, KeyEvent.VK_DOWN, KeyEvent.CHAR_UNDEFINED);
}
private void toggleSearchModeViaKeyBinding_Backwards() {
triggerKey(textField, InputEvent.CTRL_DOWN_MASK, KeyEvent.VK_UP, KeyEvent.CHAR_UNDEFINED);
}
private void showMatchingList() {
runSwing(() -> textField.showMatchingList());
}

View file

@ -428,7 +428,10 @@ public class GProperties {
}
}
catch (Exception e) {
Msg.warn(this, "Can't find field " + value + " in enum class " + enumClassName, e);
// This implies we have a saved enum value that no longer exists or we are in a branch
// that does not have the enum class that has been saved. Just emit a debug message to
// help the developer in the case that there may be a real issue.
Msg.debug(this, "Can't find field " + value + " in enum class " + enumClassName);
}
return null;
}

View file

@ -88,6 +88,18 @@ public class ImageUtils {
return newImage;
}
/**
* Pads the given image with space in the amount given.
*
* @param i the image to pad
* @param c the color to use for the padding background
* @param padding the padding
* @return a new image with the given image centered inside of padding
*/
public static Image padImage(Image i, Color c, Padding padding) {
return padImage(i, c, padding.top, padding.left, padding.right, padding.bottom);
}
/**
* Crops the given image, keeping the given bounds
*
@ -474,4 +486,15 @@ public class ImageUtils {
destination[3] = rgbPixels[3];
return destination;
}
/**
* Four int values that represent padding on each side of an image
* @param top top padding
* @param left left padding
* @param right right padding
* @param bottom bottom padding
*/
public record Padding(int top, int left, int right, int bottom) {
}
}

View file

@ -0,0 +1,36 @@
/* ###
* 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.util;
/**
* An interface that can be added to the HelpService that signals the client has help that may
* change over time. The Help system will query this class to see if there is help for the
* registered object at the time help is requested. A client may register a static help location
* and an instance of this class with the Help system.
* <p>
* This can be used by a component to change the help location based on focus or mouse interaction.
* Typically a component will have one static help location. However, if that component has help
* for different areas within the component, then this interface allows that component to return
* any active help. This is useful for components that perform custom painting of regions, in
* which case that region has no object to use for adding help to the help system.
*/
public interface DynamicHelpLocation {
/**
* @return the current help location or null if there is currently no help for the client.
*/
public HelpLocation getActiveHelpLocation();
}

View file

@ -19,8 +19,7 @@ import java.awt.*;
import javax.swing.JButton;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
import ghidra.util.*;
import help.HelpDescriptor;
import help.HelpService;
@ -64,6 +63,11 @@ public class DefaultHelpService implements HelpService {
// no-op
}
@Override
public void registerDynamicHelp(Object helpObject, DynamicHelpLocation helpLocation) {
// no-op
}
@Override
public HelpLocation getHelpLocation(Object object) {
return null;

View file

@ -18,6 +18,7 @@ package help;
import java.awt.Component;
import java.net.URL;
import ghidra.util.DynamicHelpLocation;
import ghidra.util.HelpLocation;
/**
@ -85,6 +86,14 @@ public interface HelpService {
*/
public void registerHelp(Object helpObject, HelpLocation helpLocation);
/**
* Registers a provider of dynamic help. See {@link DynamicHelpLocation} for more information.
*
* @param helpObject the object to associate the specified help location with
* @param helpLocation the dynamic help location
*/
public void registerDynamicHelp(Object helpObject, DynamicHelpLocation helpLocation);
/**
* Removes this object from the help system. This method is useful, for example,
* when a single Java {@link Component} will have different help locations

View file

@ -22,11 +22,11 @@ import ghidra.framework.plugintool.util.*;
/**
* The default plugin package provider that uses the {@link PluginsConfiguration} to supply packages
*/
public class DeafultPluginPackagingProvider implements PluginPackagingProvider {
public class DefaultPluginPackagingProvider implements PluginPackagingProvider {
private PluginsConfiguration pluginClassManager;
DeafultPluginPackagingProvider(PluginsConfiguration pluginClassManager) {
DefaultPluginPackagingProvider(PluginsConfiguration pluginClassManager) {
this.pluginClassManager = pluginClassManager;
}

View file

@ -34,7 +34,7 @@ public class PluginConfigurationModel {
public PluginConfigurationModel(PluginTool tool) {
this(new DefaultPluginInstaller(tool),
new DeafultPluginPackagingProvider(tool.getPluginsConfiguration()));
new DefaultPluginPackagingProvider(tool.getPluginsConfiguration()));
}
public PluginConfigurationModel(PluginInstaller pluginInstaller,

View file

@ -31,7 +31,9 @@ import ghidra.framework.plugintool.PluginConfigurationModel;
import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.plugintool.util.PluginPackage;
import ghidra.util.HelpLocation;
import ghidra.util.Msg;
import resources.Icons;
import utilities.util.reflection.ReflectionUtilities;
public class ManagePluginsDialog extends ReusableDialogComponentProvider {
@ -145,6 +147,18 @@ public class ManagePluginsDialog extends ReusableDialogComponentProvider {
public boolean isEnabledForContext(ActionContext context) {
return true;
}
@Override
public void setEnabled(boolean newValue) {
if (!newValue) {
Msg.debug(this, "disable Save As...",
ReflectionUtilities.createJavaFilteredThrowable());
}
super.setEnabled(newValue);
}
};
icon = Icons.SAVE_AS_ICON;
saveAsAction

View file

@ -15,9 +15,10 @@
*/
package help.screenshot;
import java.awt.Component;
import java.awt.Window;
import java.util.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import javax.swing.*;
@ -25,6 +26,8 @@ import org.junit.Test;
import docking.ComponentProvider;
import docking.DialogComponentProvider;
import docking.util.image.Callout;
import docking.util.image.CalloutInfo;
import docking.widgets.DropDownSelectionTextField;
import docking.widgets.button.BrowseButton;
import docking.widgets.tree.GTree;
@ -33,13 +36,12 @@ import ghidra.app.plugin.core.compositeeditor.*;
import ghidra.app.plugin.core.datamgr.editor.EnumEditorProvider;
import ghidra.app.plugin.core.datamgr.util.DataTypeChooserDialog;
import ghidra.app.services.DataTypeManagerService;
import ghidra.app.util.datatype.DataTypeSelectionDialog;
import ghidra.app.util.datatype.DataTypeSelectionEditor;
import ghidra.program.model.data.*;
public class DataTypeEditorsScreenShots extends GhidraScreenShotGenerator {
public DataTypeEditorsScreenShots() {
}
@Test
public void testDialog() {
@ -48,6 +50,18 @@ public class DataTypeEditorsScreenShots extends GhidraScreenShotGenerator {
captureDialog();
}
@Test
public void testDialog_SearchMode() {
positionListingTop(0x40D3B8);
performAction("Choose Data Type", "DataPlugin", false);
captureDialog();
createSearchModeCallout();
cropExcessSpace();
}
@Test
public void testDialog_Multiple_Match() throws Exception {
@ -142,6 +156,7 @@ public class DataTypeEditorsScreenShots extends GhidraScreenShotGenerator {
ComponentProvider structureEditor = getProvider(StructureEditorProvider.class);
// get structure table and select a row
@SuppressWarnings("rawtypes")
CompositeEditorPanel editorPanel =
(CompositeEditorPanel) getInstanceField("editorPanel", structureEditor);
JTable table = editorPanel.getTable();
@ -178,6 +193,7 @@ public class DataTypeEditorsScreenShots extends GhidraScreenShotGenerator {
ComponentProvider structureEditor = getProvider(StructureEditorProvider.class);
// get structure table and select a row
@SuppressWarnings("rawtypes")
CompositeEditorPanel editorPanel =
(CompositeEditorPanel) getInstanceField("editorPanel", structureEditor);
JTable table = editorPanel.getTable();
@ -203,6 +219,7 @@ public class DataTypeEditorsScreenShots extends GhidraScreenShotGenerator {
ComponentProvider structureEditor = getProvider(StructureEditorProvider.class);
// get structure table and select a row
@SuppressWarnings("rawtypes")
CompositeEditorPanel editorPanel =
(CompositeEditorPanel) getInstanceField("editorPanel", structureEditor);
JTable table = editorPanel.getTable();
@ -262,6 +279,7 @@ public class DataTypeEditorsScreenShots extends GhidraScreenShotGenerator {
ComponentProvider structureEditor = getProvider(StructureEditorProvider.class);
// get structure table and select a row
@SuppressWarnings("rawtypes")
CompositeEditorPanel editorPanel =
(CompositeEditorPanel) getInstanceField("editorPanel", structureEditor);
JTable table = editorPanel.getTable();
@ -404,4 +422,33 @@ public class DataTypeEditorsScreenShots extends GhidraScreenShotGenerator {
tool.execute(createDataCmd, program);
waitForBusyTool(tool);
}
private void cropExcessSpace() {
// keep the hover area and callout in the image (trial and error)
Rectangle area = new Rectangle();
area.x = 200;
area.y = 10;
area.width = 450;
area.height = 250;
crop(area);
}
private void createSearchModeCallout() {
DataTypeSelectionDialog dialog = waitForDialogComponent(DataTypeSelectionDialog.class);
DataTypeSelectionEditor editor = dialog.getEditor();
DropDownSelectionTextField<DataType> textField = editor.getDropDownTextField();
DropDownSelectionTextField<DataType>.SearchModeBounds searchModeBounds =
textField.getSearchModeBounds();
Rectangle hoverBounds = searchModeBounds.getHoverAreaBounds();
Window destinationComponent = SwingUtilities.windowForComponent(dialog.getComponent());
CalloutInfo calloutInfo =
new CalloutInfo(destinationComponent, textField, hoverBounds);
calloutInfo.setMagnification(2.75D); // make it a bit bigger than default
Callout callout = new Callout();
image = callout.createCalloutOnImage(image, calloutInfo);
}
}

View file

@ -35,7 +35,7 @@ import docking.action.DockingAction;
import docking.menu.ActionState;
import docking.menu.MultiStateDockingAction;
import docking.util.image.Callout;
import docking.util.image.CalloutComponentInfo;
import docking.util.image.CalloutInfo;
import docking.widgets.dialogs.MultiLineInputDialog;
import edu.uci.ics.jung.graph.Graph;
import edu.uci.ics.jung.visualization.VisualizationServer;
@ -61,14 +61,18 @@ import ghidra.util.exception.AssertException;
public class FunctionGraphPluginScreenShots extends AbstractFunctionGraphTest {
static {
// Note: this is usually done by AbstractScreenShotGenerator. The following user name
// setting needs to happen before the application is initialized. Since we don't extend
// AbstractScreenShotGenerator, we have to do it ourselves.
System.setProperty("user.name", AbstractScreenShotGenerator.SCREENSHOT_USER_NAME);
}
private MyScreen screen;
private int width = 400;
private int height = 400;
public FunctionGraphPluginScreenShots() {
super();
}
@Override
@Before
public void setUp() throws Exception {
@ -85,7 +89,7 @@ public class FunctionGraphPluginScreenShots extends AbstractFunctionGraphTest {
screen.program = program;
setLayout();
setNestedLayout();
}
@Override
@ -446,7 +450,7 @@ public class FunctionGraphPluginScreenShots extends AbstractFunctionGraphTest {
return dc.getHeader();
}
private void createCallout(JComponent parentComponent, CalloutComponentInfo calloutInfo) {
private void createCallout(JComponent parentComponent, CalloutInfo calloutInfo) {
// create image of parent with extra space for callout feature
Image parentImage = screen.captureComponent(parentComponent);
@ -458,7 +462,7 @@ public class FunctionGraphPluginScreenShots extends AbstractFunctionGraphTest {
private void createGroupButtonCallout(FGVertex v) {
JButton component = getToolbarButton(v, "Group Vertices");
JButton button = getToolbarButton(v, "Group Vertices");
FGProvider provider = screen.getProvider(FGProvider.class);
JComponent parent = provider.getComponent();
@ -466,22 +470,23 @@ public class FunctionGraphPluginScreenShots extends AbstractFunctionGraphTest {
FGView view = controller.getView();
VisualizationViewer<FGVertex, FGEdge> viewer = view.getPrimaryGraphViewer();
Rectangle bounds = component.getBounds();
Dimension size = bounds.getSize();
Point location = bounds.getLocation();
Rectangle buttonBounds = button.getBounds();
Point location = buttonBounds.getLocation();
JComponent vertexComponent = v.getComponent();
Point newLocation =
SwingUtilities.convertPoint(component.getParent(), location, vertexComponent);
Point vertexRelativeLocation =
SwingUtilities.convertPoint(button.getParent(), location, vertexComponent);
Point relativePoint = GraphViewerUtils.translatePointFromVertexRelativeSpaceToViewSpace(
viewer, v, newLocation);
Point buttonViewPoint = GraphViewerUtils.translatePointFromVertexRelativeSpaceToViewSpace(
viewer, v, vertexRelativeLocation);
Rectangle buttonArea = new Rectangle(buttonViewPoint, buttonBounds.getSize());
Point screenLocation = new Point(relativePoint);
SwingUtilities.convertPointToScreen(screenLocation, parent);
CalloutComponentInfo calloutInfo = new FGCalloutComponentInfo(parent, component,
screenLocation, relativePoint, size, viewer, v);
// Use 'parent' for both source and destination. This has the effect of not moving any
// locations, since the source and destination of the moves will be the same. For this use
// case, the locations should all be where they need to be before creating the callout info.
// It is done this way because the graph's vertices are painted as needed and are not
// connected to a real display hierarchy.
CalloutInfo calloutInfo = new CalloutInfo(parent, parent, buttonArea);
createCallout(parent, calloutInfo);
}
@ -779,28 +784,6 @@ public class FunctionGraphPluginScreenShots extends AbstractFunctionGraphTest {
return reference.get();
}
private void setNestedLayout() {
Object actionManager = getInstanceField("actionManager", graphProvider);
@SuppressWarnings("unchecked")
final MultiStateDockingAction<Class<? extends FGLayoutProvider>> action =
(MultiStateDockingAction<Class<? extends FGLayoutProvider>>) getInstanceField(
"layoutAction", actionManager);
runSwing(() -> {
List<ActionState<Class<? extends FGLayoutProvider>>> states =
action.getAllActionStates();
for (ActionState<Class<? extends FGLayoutProvider>> state : states) {
Class<? extends FGLayoutProvider> layoutClass = state.getUserData();
if (layoutClass.getSimpleName().equals("DecompilerNestedLayoutProvider")) {
action.setCurrentActionState(state);
return;
}
}
throw new RuntimeException("Could not find layout!!");
});
}
private void createGroupButtonCallout_PlayArea(final FGVertex v, final String imageName) {
FGProvider provider = screen.getProvider(FGProvider.class);
@ -832,32 +815,33 @@ public class FunctionGraphPluginScreenShots extends AbstractFunctionGraphTest {
dialog.setVisible(true);
}
@SuppressWarnings("rawtypes")
private void setLayout() {
long start = System.currentTimeMillis();
Object actionManager = getInstanceField("actionManager", graphProvider);
final MultiStateDockingAction<?> action =
(MultiStateDockingAction<?>) getInstanceField("layoutAction", actionManager);
private void setNestedLayout() {
Object minCrossState = null;
List<?> states = action.getAllActionStates();
for (Object state : states) {
if (((ActionState) state).getName().indexOf("Nested Code Layout") != -1) {
minCrossState = state;
long start = System.currentTimeMillis();
Object actionManager = getInstanceField("actionManager", graphProvider);
@SuppressWarnings("unchecked")
final MultiStateDockingAction<Class<? extends FGLayoutProvider>> action =
(MultiStateDockingAction<Class<? extends FGLayoutProvider>>) getInstanceField(
"layoutAction", actionManager);
runSwing(() -> {
List<ActionState<Class<? extends FGLayoutProvider>>> states =
action.getAllActionStates();
ActionState<Class<? extends FGLayoutProvider>> nestedCodeState = null;
for (ActionState<Class<? extends FGLayoutProvider>> state : states) {
if (state.getName().indexOf("Nested Code Layout") != -1) {
nestedCodeState = state;
break;
}
}
assertNotNull("Could not find min cross layout!", minCrossState);
assertNotNull("Could not find Nested Code Layout layout!", nestedCodeState);
//@formatter:off
invokeInstanceMethod( "setCurrentActionState",
action,
new Class<?>[] { ActionState.class },
new Object[] { minCrossState });
//@formatter:on
action.setCurrentActionState(nestedCodeState);
runSwing(() -> action.actionPerformed(new DefaultActionContext()));
// action.actionPerformed(new DefaultActionContext())
});
// wait for the threaded graph layout code
FGController controller = getFunctionGraphController();
@ -868,6 +852,13 @@ public class FunctionGraphPluginScreenShots extends AbstractFunctionGraphTest {
long end = System.currentTimeMillis();
Msg.debug(this, "relayout time: " + ((end - start) / 1000.0) + "s");
}
@Override
protected void installTestGraphLayout(FGProvider provider) {
// Do nothing. The normal tests will install a test layout in this method. We don't need
// that behavior.
}
//==================================================================================================
@ -897,28 +888,4 @@ public class FunctionGraphPluginScreenShots extends AbstractFunctionGraphTest {
return helpTopicDir;
}
}
private class FGCalloutComponentInfo extends CalloutComponentInfo {
private VisualizationViewer<FGVertex, FGEdge> viewer;
private FGVertex vertex;
FGCalloutComponentInfo(Component destinationComponent, Component component,
Point locationOnScreen, Point relativeLocation, Dimension size,
VisualizationViewer<FGVertex, FGEdge> viewer, FGVertex vertex) {
super(destinationComponent, component, locationOnScreen, relativeLocation, size);
this.viewer = viewer;
this.vertex = vertex;
}
@Override
public Point convertPointToParent(Point location) {
// TODO: this won't work for now if the graph is scaled. This is because there is
// point information that is calculated by the client of this class that does
// not take into account the scaling of the graph. This is a known issue--
// don't use this class when the graph is scaled.
return location;
}
}
}

View file

@ -24,11 +24,10 @@ import javax.swing.table.JTableHeader;
import org.junit.Test;
import docking.DockableComponent;
import docking.menu.MultiStateDockingAction;
import docking.util.AnimationUtils;
import docking.util.image.Callout;
import docking.util.image.CalloutComponentInfo;
import docking.util.image.CalloutInfo;
import docking.widgets.EmptyBorderButton;
import docking.widgets.filter.*;
import docking.widgets.table.columnfilter.ColumnBasedTableFilter;
@ -88,11 +87,15 @@ public class TreesScreenShots extends GhidraScreenShotGenerator {
component we provide. But, we need to be able to translate that component's
location to a value that is relative to the image (we created the image above by
capturing the provider using it's DockableComponent).
Important!: since we only captured the provider and not the window, we need to pass in
the dockable component, which is the same bounds as the provider. If we pass the parent
window, then we will be off in the y direction in the amount of all the items above the
dockable component, such as the window bar, the menu bar, etc.
*/
DockableComponent dc = getDockableComponent(provider);
CalloutComponentInfo calloutInfo = new CalloutComponentInfo(dc, label);
Component dc = getDockableComponent(provider);
CalloutInfo calloutInfo = new CalloutInfo(dc, label);
calloutInfo.setMagnification(2.75D); // make it a bit bigger than default
Callout callout = new Callout();
image = callout.createCalloutOnImage(image, calloutInfo);
@ -104,8 +107,8 @@ public class TreesScreenShots extends GhidraScreenShotGenerator {
Rectangle area = new Rectangle();
int height = 275;
area.x = 0;
area.y = 80;
area.width = 560;
area.y = 60;
area.width = 580;
area.height = height - area.y;
crop(area);
}