(#$xQfF++$dH!@q0y
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
Jm&(2l{1+G$)v5ph
diff --git a/Ghidra/Features/Base/src/main/help/help/topics/DataTypeEditors/images/Dialog_SearchMode.png b/Ghidra/Features/Base/src/main/help/help/topics/DataTypeEditors/images/Dialog_SearchMode.png
new file mode 100644
index 0000000000000000000000000000000000000000..6b6d0b7b9899a7add61816a0b86c6217048aaa8f
GIT binary patch
literal 20717
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
literal 0
HcmV?d00001
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/RegisterDropDownSelectionDataModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/RegisterDropDownSelectionDataModel.java
index dedef9c5f9..7b1f9f4439 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/RegisterDropDownSelectionDataModel.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/function/editor/RegisterDropDownSelectionDataModel.java
@@ -18,8 +18,11 @@ package ghidra.app.plugin.core.function.editor;
import java.util.ArrayList;
import java.util.List;
+import javax.help.UnsupportedOperationException;
import javax.swing.ListCellRenderer;
+import org.apache.commons.lang3.StringUtils;
+
import docking.widgets.DropDownSelectionTextField;
import docking.widgets.DropDownTextFieldDataModel;
import docking.widgets.list.GListCellRenderer;
@@ -37,6 +40,11 @@ public class RegisterDropDownSelectionDataModel implements DropDownTextFieldData
this.registers = registers;
}
+ @Override
+ public List getSupportedSearchModes() {
+ return List.of(SearchMode.STARTS_WITH);
+ }
+
@Override
public ListCellRenderer getListRenderer() {
return new GListCellRenderer();
@@ -54,11 +62,20 @@ public class RegisterDropDownSelectionDataModel implements DropDownTextFieldData
@Override
public List getMatchingData(String searchText) {
+ throw new UnsupportedOperationException(
+ "Method no longer supported. Instead, call getMatchingData(String, SearchMode)");
+ }
- if (searchText == null || searchText.length() == 0) {
+ @Override
+ public List getMatchingData(String searchText, SearchMode searchMode) {
+ if (StringUtils.isBlank(searchText)) {
return registers;
}
+ if (searchMode != SearchMode.STARTS_WITH) {
+ throw new IllegalArgumentException("Unsupported SearchMode: " + searchMode);
+ }
+
searchText = searchText.toLowerCase();
List regList = new ArrayList<>();
@@ -85,5 +102,4 @@ public class RegisterDropDownSelectionDataModel implements DropDownTextFieldData
}
return 0;
}
-
}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/ScriptSelectionEditor.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/ScriptSelectionEditor.java
index fa07c0bd55..fbc7c47748 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/ScriptSelectionEditor.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/script/ScriptSelectionEditor.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.
@@ -16,8 +16,6 @@
package ghidra.app.plugin.core.script;
import java.util.*;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
import javax.swing.*;
import javax.swing.event.*;
@@ -28,7 +26,6 @@ import docking.widgets.*;
import generic.theme.GThemeDefaults.Colors.Palette;
import ghidra.app.script.ScriptInfo;
import ghidra.util.HTMLUtilities;
-import ghidra.util.UserSearchUtils;
/**
* A widget that allows the user to choose an existing script by typing its name or picking it
@@ -222,24 +219,10 @@ public class ScriptSelectionEditor {
}
@Override
- public List getMatchingData(String searchText) {
-
- // This pattern will: 1) allow users to match the typed text anywhere in the
- // script names and 2) allow the use of globbing characters
- Pattern pattern = UserSearchUtils.createContainsPattern(searchText, true,
- Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
-
- List results = new ArrayList<>();
- for (ScriptInfo info : data) {
- String name = info.getName();
- Matcher m = pattern.matcher(name);
- if (m.matches()) {
- results.add(info);
- }
- }
-
- return results;
+ public List getSupportedSearchModes() {
+ return List.of(SearchMode.CONTAINS, SearchMode.WILDCARD, SearchMode.STARTS_WITH);
}
+
}
private class ScriptSelectionTextField extends DropDownSelectionTextField {
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/CategoryPathSelectionEditor.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/CategoryPathSelectionEditor.java
index 183822a561..83d6f7a7b7 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/CategoryPathSelectionEditor.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/CategoryPathSelectionEditor.java
@@ -18,14 +18,18 @@ package ghidra.app.util.datatype;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
-import java.util.Collections;
import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.help.UnsupportedOperationException;
import javax.swing.*;
import javax.swing.border.Border;
import javax.swing.event.*;
import javax.swing.tree.TreePath;
+import org.apache.commons.lang3.StringUtils;
+
import docking.widgets.DropDownSelectionTextField;
import docking.widgets.DropDownTextFieldDataModel;
import docking.widgets.button.BrowseButton;
@@ -406,16 +410,37 @@ public class CategoryPathSelectionEditor extends AbstractCellEditor {
return categoryPath.getPath();
}
+ @Override
+ public List getSupportedSearchModes() {
+ return List.of(SearchMode.CONTAINS, SearchMode.STARTS_WITH, SearchMode.WILDCARD);
+ }
+
@Override
public List getMatchingData(String searchText) {
- if (searchText == null || searchText.length() == 0) {
- return Collections.emptyList();
+ 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);
+ }
+
+ Pattern p = mode.createPattern(searchText);
+ return getMatchingDataRegex(p);
+ }
+
+ private List getMatchingDataRegex(Pattern p) {
List results = new ArrayList<>();
for (CategoryPath path : data) {
String pathString = path.getPath();
- if (pathString.contains(searchText)) {
+ Matcher m = p.matcher(pathString);
+ if (m.matches()) {
results.add(path);
}
}
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/DataTypeDropDownSelectionDataModel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/DataTypeDropDownSelectionDataModel.java
index 53b39223f5..dd55039285 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/DataTypeDropDownSelectionDataModel.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/DataTypeDropDownSelectionDataModel.java
@@ -17,7 +17,10 @@ package ghidra.app.util.datatype;
import java.awt.Component;
import java.util.*;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import javax.help.UnsupportedOperationException;
import javax.swing.*;
import docking.widgets.DropDownSelectionTextField;
@@ -69,6 +72,11 @@ public class DataTypeDropDownSelectionDataModel implements DropDownTextFieldData
return service;
}
+ @Override
+ public List getSupportedSearchModes() {
+ return List.of(SearchMode.STARTS_WITH, SearchMode.CONTAINS, SearchMode.WILDCARD);
+ }
+
@Override
public ListCellRenderer getListRenderer() {
return new DataTypeDropDownRenderer();
@@ -86,13 +94,47 @@ public class DataTypeDropDownSelectionDataModel implements DropDownTextFieldData
@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 (searchText == null || searchText.length() == 0) {
+ // full list results not supported since the data may be too large for user interaction
return Collections.emptyList();
}
- List dataTypeList =
+ if (!getSupportedSearchModes().contains(mode)) {
+ throw new IllegalArgumentException("Unsupported SearchMode: " + mode);
+ }
+
+ if (mode == SearchMode.STARTS_WITH) {
+ return getMatchDataStartsWith(searchText);
+ }
+
+ Pattern p = mode.createPattern(searchText);
+ return getMatchingDataRegex(p);
+ }
+
+ private List getMatchDataStartsWith(String searchText) {
+ List results =
DataTypeUtils.getStartsWithMatchingDataTypes(searchText, dataTypeService);
- return filterDataTypeList(dataTypeList);
+ return filterDataTypeList(results);
+ }
+
+ private List getMatchingDataRegex(Pattern p) {
+
+ List results = new ArrayList<>();
+ List allTypes = dataTypeService.getSortedDataTypeList();
+ for (DataType dt : allTypes) {
+ String name = dt.getName().toLowerCase();
+ Matcher m = p.matcher(name);
+ if (m.matches()) {
+ results.add(dt);
+ }
+ }
+ return filterDataTypeList(results);
}
/**
diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/DataTypeSelectionEditor.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/DataTypeSelectionEditor.java
index 5dff65e417..b9827dd49c 100644
--- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/DataTypeSelectionEditor.java
+++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/datatype/DataTypeSelectionEditor.java
@@ -151,6 +151,8 @@ public class DataTypeSelectionEditor extends AbstractCellEditor {
editorPanel.add(selectionField);
editorPanel.add(browsePanel);
+ // This listener is not installed under certain conditions, such as when
+ // setTabCommitsEdit(true) is called.
keyListener = new KeyAdapter() {
@Override
diff --git a/Ghidra/Features/Base/src/main/java/help/screenshot/AbstractScreenShotGenerator.java b/Ghidra/Features/Base/src/main/java/help/screenshot/AbstractScreenShotGenerator.java
index 6090e8304b..b3ddf032a3 100644
--- a/Ghidra/Features/Base/src/main/java/help/screenshot/AbstractScreenShotGenerator.java
+++ b/Ghidra/Features/Base/src/main/java/help/screenshot/AbstractScreenShotGenerator.java
@@ -87,7 +87,7 @@ import resources.ResourceManager;
public abstract class AbstractScreenShotGenerator extends AbstractGhidraHeadedIntegrationTest {
- private static final String SCREENSHOT_USER_NAME = "User-1";
+ protected static final String SCREENSHOT_USER_NAME = "User-1";
static {
System.setProperty("user.name", "User-1");
diff --git a/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/AbstractFunctionGraphTest.java b/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/AbstractFunctionGraphTest.java
index 997db3fa94..36b3664d04 100644
--- a/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/AbstractFunctionGraphTest.java
+++ b/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/AbstractFunctionGraphTest.java
@@ -783,7 +783,7 @@ public abstract class AbstractFunctionGraphTest extends AbstractGhidraHeadedInte
waitForSwing();
- int tryCount = 3;
+ int tryCount = 0;
while (tryCount++ < 5 && updater.isBusy()) {
waitForConditionWithoutFailing(() -> !updater.isBusy());
}
diff --git a/Ghidra/Framework/Docking/src/main/java/docking/help/HelpManager.java b/Ghidra/Framework/Docking/src/main/java/docking/help/HelpManager.java
index 99e3100b7c..abd7baa20c 100644
--- a/Ghidra/Framework/Docking/src/main/java/docking/help/HelpManager.java
+++ b/Ghidra/Framework/Docking/src/main/java/docking/help/HelpManager.java
@@ -72,6 +72,7 @@ public class HelpManager implements HelpService {
private HashMap urlToHelpSets = new HashMap<>();
private Map helpLocations = new WeakHashMap<>();
+ private Map dynamicHelp = new WeakHashMap<>();
private List 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