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

@ -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

@ -4,9 +4,9 @@
* 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.
@ -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

@ -4,9 +4,9 @@
* 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.
@ -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,7 +566,13 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
Cursor previousCursor = getCursor();
try {
setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
return dataModel.getMatchingData(searchText);
if (searchMode == SearchMode.UNKNOWN) {
// backward compatible
return dataModel.getMatchingData(searchText);
}
return dataModel.getMatchingData(searchText, searchMode);
}
finally {
setCursor(previousCursor);
@ -383,12 +588,13 @@ 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() {
//
// We temporarily enable this list to show for empty text, even if the text is not empty.
// We temporarily enable this list to show for empty text, even if the text is not empty.
// This handles the default setting, which has this feature off. We can refactor this class
// to allow us to make a direct call instead of using this temporary setting. This seems
// simple enough for now.
@ -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,21 +1162,33 @@ 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();
boolean forward = keyCode == KeyEvent.VK_DOWN || keyCode == KeyEvent.VK_KP_DOWN;
toggleSearchMode(forward);
return;
}
updateDisplayContents(getText());
event.consume();
}
private void handleArrowKeyForMatchingWindow(KeyEvent event) {
int keyCode = event.getKeyCode();
if (!getMatchingWindow().isShowing()) {
updateDisplayContents(getText());
event.consume();
if (keyCode == KeyEvent.VK_UP || keyCode == KeyEvent.VK_KP_UP) {
decrementListSelection();
}
else { // update the window if it is showing
if (keyCode == KeyEvent.VK_UP || keyCode == KeyEvent.VK_KP_UP) {
decrementListSelection();
}
else {
incrementListSelection();
}
event.consume();
setTextFromSelectedListItemAndKeepMatchingWindowOpen();
else {
incrementListSelection();
}
event.consume();
setTextFromSelectedListItemAndKeepMatchingWindowOpen();
}
private void incrementListSelection() {
@ -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

@ -4,9 +4,9 @@
* 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.
@ -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;
}
/**
* 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 match typically
* means a "startsWith" match. A list is returned to allow for multiple matches.
* 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

@ -4,9 +4,9 @@
* 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.
@ -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

@ -4,9 +4,9 @@
* 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.
@ -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

@ -4,9 +4,9 @@
* 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.
@ -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();
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);
// we expect the location to change, but there may be a delay
waitForCondition(() -> {
JWindow currentMatchingWindow = textField.getActiveMatchingWindow();
Point newLocation = runSwing(() -> currentMatchingWindow.getLocationOnScreen());
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());
}