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) {
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/util/image/Callout.java b/Ghidra/Framework/Docking/src/main/java/docking/util/image/Callout.java
index 659ff09ea3..80d92e43f2 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/util/image/Callout.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/util/image/Callout.java
@@ -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();
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/util/image/CalloutComponentInfo.java b/Ghidra/Framework/Docking/src/main/java/docking/util/image/CalloutComponentInfo.java
deleted file mode 100644
index e70074b455..0000000000
--- a/Ghidra/Framework/Docking/src/main/java/docking/util/image/CalloutComponentInfo.java
+++ /dev/null
@@ -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).
- *
- * 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;
- }
-}
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/util/image/CalloutInfo.java b/Ghidra/Framework/Docking/src/main/java/docking/util/image/CalloutInfo.java
new file mode 100644
index 0000000000..57e491ff3d
--- /dev/null
+++ b/Ghidra/Framework/Docking/src/main/java/docking/util/image/CalloutInfo.java
@@ -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).
+ *
+ *
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.
+ *
+ * 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.
+ *
+ * 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 client space 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 client space. 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
+ * client space 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);
+ }
+}
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/util/image/DropShadow.java b/Ghidra/Framework/Docking/src/main/java/docking/util/image/DropShadow.java
index cc9831507c..dfdeddbfbf 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/util/image/DropShadow.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/util/image/DropShadow.java
@@ -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));
-// }
-
}
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/DefaultDropDownSelectionDataModel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/DefaultDropDownSelectionDataModel.java
index 9d68a7eea1..17638fcc32 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/DefaultDropDownSelectionDataModel.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/DefaultDropDownSelectionDataModel.java
@@ -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 implements DropDownTextFieldDa
Collections.sort(data, comparator);
}
+ @Override
+ public List getSupportedSearchModes() {
+ return List.of(SearchMode.STARTS_WITH, SearchMode.CONTAINS, SearchMode.WILDCARD);
+ }
+
@Override
public List getMatchingData(String searchText) {
+ throw new UnsupportedOperationException(
+ "Method no longer supported. Instead, call getMatchingData(String, SearchMode)");
+ }
+
+ @Override
+ public List 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 getMatchingDataRegex(Pattern p) {
+ List 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 getMatchingDataStartsWith(String searchText) {
List> l = data;
int startIndex = Collections.binarySearch(l, (Object) searchText, comparator);
int endIndex = Collections.binarySearch(l, (Object) (searchText + END_CHAR), comparator);
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/DropDownTextField.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/DropDownTextField.java
index af22255b89..0f388823a5 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/DropDownTextField.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/DropDownTextField.java
@@ -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 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 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 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 searchModes = new ArrayList<>();
+ private boolean searchModeIsHovered;
+ private SearchMode searchMode = SearchMode.UNKNOWN;
+ private SearchModeBounds searchModeBounds;
+
/**
* Constructor.
*
@@ -132,7 +154,36 @@ public class DropDownTextField 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 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 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 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 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 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 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 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 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 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 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);
+ }
+ }
+
}
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/DropDownTextFieldDataModel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/DropDownTextFieldDataModel.java
index 7a9117f053..d2ff4d6f4d 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/DropDownTextFieldDataModel.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/DropDownTextFieldDataModel.java
@@ -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 {
+ 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 searchText
. 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 searchText
. 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.
+ *
+ * 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 getMatchingData(String searchText);
+ /**
+ * Returns a list of data that matches the given searchText
. 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.
+ *
+ * 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 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 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
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/FileDropDownSelectionDataModel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/FileDropDownSelectionDataModel.java
index fa6accee34..fa0d5a2b17 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/FileDropDownSelectionDataModel.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/FileDropDownSelectionDataModel.java
@@ -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 getSupportedSearchModes() {
+ return List.of(SearchMode.STARTS_WITH, SearchMode.CONTAINS, SearchMode.WILDCARD);
+ }
+
@Override
public List getMatchingData(String searchText) {
- if (searchText == null || searchText.length() == 0) {
+ throw new UnsupportedOperationException(
+ "Method no longer supported. Instead, call getMatchingData(String, SearchMode)");
+ }
+
+ @Override
+ public List 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 getMatchingDataRegex(Pattern p) {
+
+ List matches = new ArrayList<>();
+ List 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 getMatchDataStartsWith(String searchText) {
+ List list = getSortedFiles();
+ return getMatchingSubList(searchText, searchText + END_CHAR, list);
+ }
+
+ private List 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 getMatchingSubList(String searchTextStart, String searchTextEnd,
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/AutocompletingStringConstraintEditor.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/AutocompletingStringConstraintEditor.java
index 860cc32102..eae64b5a46 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/AutocompletingStringConstraintEditor.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/constrainteditor/AutocompletingStringConstraintEditor.java
@@ -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 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());
diff --git a/Ghidra/Framework/Docking/src/test/java/docking/widgets/AbstractDropDownTextFieldTest.java b/Ghidra/Framework/Docking/src/test/java/docking/widgets/AbstractDropDownTextFieldTest.java
index 74707e8884..16679b8cd9 100644
--- a/Ghidra/Framework/Docking/src/test/java/docking/widgets/AbstractDropDownTextFieldTest.java
+++ b/Ghidra/Framework/Docking/src/test/java/docking/widgets/AbstractDropDownTextFieldTest.java
@@ -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 extends AbstractDockingTest {
@@ -151,21 +151,30 @@ public abstract class AbstractDropDownTextFieldTest 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 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 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 extends AbstractDockingTe
assertNull(actual);
}
+ protected void assertMatchesInList(String... expected) {
+
+ waitForSwing();
+ assertMatchingWindowShowing();
+
+ @SuppressWarnings("unchecked")
+ JList list = (JList) textField.getJList();
+ ListModel model = list.getModel();
+ int n = model.getSize();
+ assertEquals("Expected item size is not the same as the matching list size",
+ expected.length, n);
+ HashSet 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 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 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);
diff --git a/Ghidra/Framework/Docking/src/test/java/docking/widgets/DefaultDropDownSelectionDataModelTest.java b/Ghidra/Framework/Docking/src/test/java/docking/widgets/DefaultDropDownSelectionDataModelTest.java
index 4fde9ac487..1d39bf5e1d 100644
--- a/Ghidra/Framework/Docking/src/test/java/docking/widgets/DefaultDropDownSelectionDataModelTest.java
+++ b/Ghidra/Framework/Docking/src/test/java/docking/widgets/DefaultDropDownSelectionDataModelTest.java
@@ -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 matchingData = model.getMatchingData("a");
+ List 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());
diff --git a/Ghidra/Framework/Docking/src/test/java/docking/widgets/DropDownTextFieldTest.java b/Ghidra/Framework/Docking/src/test/java/docking/widgets/DropDownTextFieldTest.java
index c3cf8c9d2a..bb379e3eb4 100644
--- a/Ghidra/Framework/Docking/src/test/java/docking/widgets/DropDownTextFieldTest.java
+++ b/Ghidra/Framework/Docking/src/test/java/docking/widgets/DropDownTextFieldTest.java
@@ -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
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
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.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());
}
diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/GProperties.java b/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/GProperties.java
index fe9500c689..7cd4fa87ac 100644
--- a/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/GProperties.java
+++ b/Ghidra/Framework/Generic/src/main/java/ghidra/framework/options/GProperties.java
@@ -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;
}
diff --git a/Ghidra/Framework/Gui/src/main/java/generic/util/image/ImageUtils.java b/Ghidra/Framework/Gui/src/main/java/generic/util/image/ImageUtils.java
index d43441e2a1..aa50193536 100644
--- a/Ghidra/Framework/Gui/src/main/java/generic/util/image/ImageUtils.java
+++ b/Ghidra/Framework/Gui/src/main/java/generic/util/image/ImageUtils.java
@@ -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) {
+
+ }
}
diff --git a/Ghidra/Framework/Gui/src/main/java/ghidra/util/DynamicHelpLocation.java b/Ghidra/Framework/Gui/src/main/java/ghidra/util/DynamicHelpLocation.java
new file mode 100644
index 0000000000..4e4b21da36
--- /dev/null
+++ b/Ghidra/Framework/Gui/src/main/java/ghidra/util/DynamicHelpLocation.java
@@ -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.
+ *
+ * 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();
+}
diff --git a/Ghidra/Framework/Help/src/main/java/docking/DefaultHelpService.java b/Ghidra/Framework/Help/src/main/java/docking/DefaultHelpService.java
index b103ee06d0..993d23009d 100644
--- a/Ghidra/Framework/Help/src/main/java/docking/DefaultHelpService.java
+++ b/Ghidra/Framework/Help/src/main/java/docking/DefaultHelpService.java
@@ -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.
@@ -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;
diff --git a/Ghidra/Framework/Help/src/main/java/help/HelpService.java b/Ghidra/Framework/Help/src/main/java/help/HelpService.java
index 969c97730a..d6aff75c31 100644
--- a/Ghidra/Framework/Help/src/main/java/help/HelpService.java
+++ b/Ghidra/Framework/Help/src/main/java/help/HelpService.java
@@ -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,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
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/DeafultPluginPackagingProvider.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/DefaultPluginPackagingProvider.java
similarity index 92%
rename from Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/DeafultPluginPackagingProvider.java
rename to Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/DefaultPluginPackagingProvider.java
index b6fe7bdfcd..0c524dd90b 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/DeafultPluginPackagingProvider.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/DefaultPluginPackagingProvider.java
@@ -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.
@@ -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;
}
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginConfigurationModel.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginConfigurationModel.java
index 23f2e9cb44..e14a3be562 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginConfigurationModel.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/PluginConfigurationModel.java
@@ -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.
@@ -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,
diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/ManagePluginsDialog.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/ManagePluginsDialog.java
index 2ba35d9df8..de5653df0e 100644
--- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/ManagePluginsDialog.java
+++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/ManagePluginsDialog.java
@@ -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
diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DataTypeEditorsScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DataTypeEditorsScreenShots.java
index 4e7dc09fd0..f1b5457697 100644
--- a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DataTypeEditorsScreenShots.java
+++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/DataTypeEditorsScreenShots.java
@@ -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,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 textField = editor.getDropDownTextField();
+ DropDownSelectionTextField.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);
+ }
+
}
diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/FunctionGraphPluginScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/FunctionGraphPluginScreenShots.java
index 2d0998d197..c8a888273b 100644
--- a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/FunctionGraphPluginScreenShots.java
+++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/FunctionGraphPluginScreenShots.java
@@ -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 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> action =
- (MultiStateDockingAction>) getInstanceField(
- "layoutAction", actionManager);
- runSwing(() -> {
- List>> states =
- action.getAllActionStates();
- for (ActionState> 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() {
+ private void setNestedLayout() {
+
long start = System.currentTimeMillis();
+
Object actionManager = getInstanceField("actionManager", graphProvider);
- final MultiStateDockingAction> action =
- (MultiStateDockingAction>) getInstanceField("layoutAction", actionManager);
+ @SuppressWarnings("unchecked")
+ final MultiStateDockingAction> action =
+ (MultiStateDockingAction>) getInstanceField(
+ "layoutAction", actionManager);
+ runSwing(() -> {
+ List>> states =
+ action.getAllActionStates();
- Object minCrossState = null;
- List> states = action.getAllActionStates();
- for (Object state : states) {
- if (((ActionState) state).getName().indexOf("Nested Code Layout") != -1) {
- minCrossState = state;
- break;
+ ActionState> nestedCodeState = null;
+ for (ActionState> 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 viewer;
- private FGVertex vertex;
-
- FGCalloutComponentInfo(Component destinationComponent, Component component,
- Point locationOnScreen, Point relativeLocation, Dimension size,
- VisualizationViewer 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;
- }
- }
}
diff --git a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/TreesScreenShots.java b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/TreesScreenShots.java
index 951b02b580..3c7122f450 100644
--- a/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/TreesScreenShots.java
+++ b/Ghidra/Test/IntegrationTest/src/screen/java/help/screenshot/TreesScreenShots.java
@@ -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.
@@ -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);
}