diff --git a/Ghidra/Features/Base/certification.manifest b/Ghidra/Features/Base/certification.manifest index 76a4b1aa3b..6cafe45dfb 100644 --- a/Ghidra/Features/Base/certification.manifest +++ b/Ghidra/Features/Base/certification.manifest @@ -317,6 +317,7 @@ src/main/help/help/topics/DataTypeEditors/images/BytesNumberInputDialog.png||GHI src/main/help/help/topics/DataTypeEditors/images/Dialog.png||GHIDRA||||END| src/main/help/help/topics/DataTypeEditors/images/Dialog_Create_Pointer.png||GHIDRA||||END| src/main/help/help/topics/DataTypeEditors/images/Dialog_Multiple_Match.png||GHIDRA||||END| +src/main/help/help/topics/DataTypeEditors/images/Dialog_SearchMode.png||GHIDRA||||END| src/main/help/help/topics/DataTypeEditors/images/Dialog_Select_Tree.png||GHIDRA||||END| src/main/help/help/topics/DataTypeEditors/images/Dialog_Single_Match.png||GHIDRA||||END| src/main/help/help/topics/DataTypeEditors/images/EnumEditor.png||GHIDRA||||END| diff --git a/Ghidra/Features/Base/src/main/help/help/topics/DataTypeEditors/DataTypeSelectionDialog.htm b/Ghidra/Features/Base/src/main/help/help/topics/DataTypeEditors/DataTypeSelectionDialog.htm index ae170f8d16..8b9542ee23 100644 --- a/Ghidra/Features/Base/src/main/help/help/topics/DataTypeEditors/DataTypeSelectionDialog.htm +++ b/Ghidra/Features/Base/src/main/help/help/topics/DataTypeEditors/DataTypeSelectionDialog.htm @@ -19,6 +19,59 @@


Data Type Chooser Dialog

+ +

As you type text in the field, any potential matches will be displayed in the completion + window, which is described below. +

+ + +

+ The way matches are determined depends upon the search + mode you are in. The current mode is displayed at the right side of the text field, + indicated with a single character. Hovering over the character will show a tool tip + window that shows the name for the current mode. +

+ +
+
+

To change the search mode, click on + the seach mode character at the right side of the text field. +

+ +

+ You can also change the search mode using Ctrl Down and Ctrl Up to + change the mode forward and backward, respectively. +

+ +


+ Data Type Chooser Dialog

+
+
+ +

+ By default, this chooser uses a Starts With matching mode. Any text typed will be + used to match all data type with a name that begins with the current search text. +

+ +
+
+

This data type selection chooser + performs the best with the 'starts with' setting. For a large number of data types, + this is the recommended search setting. +

+
+
+ +
+
+

The text used to match is + based on the cursor position in the field. All text from the beginning up to the + cursor position will be used for the match. This allows you to arrow left and right + to control the matching list. +

+
+
+

Completion Window

diff --git a/Ghidra/Features/Base/src/main/help/help/topics/DataTypeEditors/images/Dialog.png b/Ghidra/Features/Base/src/main/help/help/topics/DataTypeEditors/images/Dialog.png index 903e3f3613..513d5abe76 100644 Binary files a/Ghidra/Features/Base/src/main/help/help/topics/DataTypeEditors/images/Dialog.png and b/Ghidra/Features/Base/src/main/help/help/topics/DataTypeEditors/images/Dialog.png differ 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 0000000000..6b6d0b7b98 Binary files /dev/null and b/Ghidra/Features/Base/src/main/help/help/topics/DataTypeEditors/images/Dialog_SearchMode.png differ 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 c8e332badb..ca991f3735 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 @@ -787,7 +787,7 @@ public abstract class AbstractFunctionGraphTest extends AbstractGhidraHeadedInte waitForSwing(); - int tryCount = 3; + int tryCount = 0; while (tryCount++ < 5 && updater.isBusy()) { waitForConditionWithoutFailing(() -> !updater.isBusy()); } 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 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 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); }