Merge remote-tracking branch

'origin/GP-5720-dragonmacher-drop-down-field-contains-mode--SQUASHED'
(Closes #4725, Closes #8203)
This commit is contained in:
Ryan Kurtz
2025-08-19 12:59:16 -04:00
35 changed files with 1629 additions and 676 deletions
@@ -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.png||GHIDRA||||END|
src/main/help/help/topics/DataTypeEditors/images/Dialog_Create_Pointer.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_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_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/Dialog_Single_Match.png||GHIDRA||||END|
src/main/help/help/topics/DataTypeEditors/images/EnumEditor.png||GHIDRA||||END| src/main/help/help/topics/DataTypeEditors/images/EnumEditor.png||GHIDRA||||END|
@@ -19,6 +19,59 @@
<P align="center"><IMG border="0" src="images/Dialog.png" alt=""><BR> <P align="center"><IMG border="0" src="images/Dialog.png" alt=""><BR>
<I>Data Type Chooser Dialog</I></P> <I>Data Type Chooser Dialog</I></P>
<P>As you type text in the field, any potential matches will be displayed in the completion
window, which is described below.
</P>
<A NAME="SearchMode" />
<P>
The way matches are determined depends upon the search
mode you are in. The current mode is displayed at the right side of the text field,
indicated with a single character. Hovering over the character will show a tool tip
window that shows the name for the current mode.
</P>
<BLOCKQUOTE>
<BLOCKQUOTE>
<P><IMG border="0" src="help/shared/tip.png" alt="">To change the search mode, click on
the seach mode character at the right side of the text field.
</P>
<P>
You can also change the search mode using <B>Ctrl Down</B> and <B>Ctrl Up</B> to
change the mode forward and backward, respectively.
</P>
<P align="center"><IMG border="0" src="images/Dialog_SearchMode.png" alt=""><BR>
<I>Data Type Chooser Dialog</I></P>
</BLOCKQUOTE>
</BLOCKQUOTE>
<P>
By default, this chooser uses a <B>Starts With</B> matching mode. Any text typed will be
used to match all data type with a name that begins with the current search text.
</P>
<BLOCKQUOTE>
<BLOCKQUOTE>
<P><IMG border="0" src="help/shared/tip.png" alt="">This data type selection chooser
performs the best with the 'starts with' setting. For a large number of data types,
this is the recommended search setting.
</P>
</BLOCKQUOTE>
</BLOCKQUOTE>
<BLOCKQUOTE>
<BLOCKQUOTE>
<P><IMG border="0" src="help/shared/note.yellow.png" alt="">The text used to match is
based on the cursor position in the field. All text from the beginning up to the
cursor position will be used for the match. This allows you to arrow left and right
to control the matching list.
</P>
</BLOCKQUOTE>
</BLOCKQUOTE>
<H2><A name="completion"></A> Completion Window</H2> <H2><A name="completion"></A> Completion Window</H2>
<BLOCKQUOTE> <BLOCKQUOTE>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

@@ -18,8 +18,11 @@ package ghidra.app.plugin.core.function.editor;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.help.UnsupportedOperationException;
import javax.swing.ListCellRenderer; import javax.swing.ListCellRenderer;
import org.apache.commons.lang3.StringUtils;
import docking.widgets.DropDownSelectionTextField; import docking.widgets.DropDownSelectionTextField;
import docking.widgets.DropDownTextFieldDataModel; import docking.widgets.DropDownTextFieldDataModel;
import docking.widgets.list.GListCellRenderer; import docking.widgets.list.GListCellRenderer;
@@ -37,6 +40,11 @@ public class RegisterDropDownSelectionDataModel implements DropDownTextFieldData
this.registers = registers; this.registers = registers;
} }
@Override
public List<SearchMode> getSupportedSearchModes() {
return List.of(SearchMode.STARTS_WITH);
}
@Override @Override
public ListCellRenderer<Register> getListRenderer() { public ListCellRenderer<Register> getListRenderer() {
return new GListCellRenderer<Register>(); return new GListCellRenderer<Register>();
@@ -54,11 +62,20 @@ public class RegisterDropDownSelectionDataModel implements DropDownTextFieldData
@Override @Override
public List<Register> getMatchingData(String searchText) { public List<Register> getMatchingData(String searchText) {
throw new UnsupportedOperationException(
"Method no longer supported. Instead, call getMatchingData(String, SearchMode)");
}
if (searchText == null || searchText.length() == 0) { @Override
public List<Register> getMatchingData(String searchText, SearchMode searchMode) {
if (StringUtils.isBlank(searchText)) {
return registers; return registers;
} }
if (searchMode != SearchMode.STARTS_WITH) {
throw new IllegalArgumentException("Unsupported SearchMode: " + searchMode);
}
searchText = searchText.toLowerCase(); searchText = searchText.toLowerCase();
List<Register> regList = new ArrayList<>(); List<Register> regList = new ArrayList<>();
@@ -85,5 +102,4 @@ public class RegisterDropDownSelectionDataModel implements DropDownTextFieldData
} }
return 0; return 0;
} }
} }
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -16,8 +16,6 @@
package ghidra.app.plugin.core.script; package ghidra.app.plugin.core.script;
import java.util.*; import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.swing.*; import javax.swing.*;
import javax.swing.event.*; import javax.swing.event.*;
@@ -28,7 +26,6 @@ import docking.widgets.*;
import generic.theme.GThemeDefaults.Colors.Palette; import generic.theme.GThemeDefaults.Colors.Palette;
import ghidra.app.script.ScriptInfo; import ghidra.app.script.ScriptInfo;
import ghidra.util.HTMLUtilities; 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 * 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 @Override
public List<ScriptInfo> getMatchingData(String searchText) { public List<SearchMode> getSupportedSearchModes() {
return List.of(SearchMode.CONTAINS, SearchMode.WILDCARD, SearchMode.STARTS_WITH);
// This pattern will: 1) allow users to match the typed text anywhere in the
// script names and 2) allow the use of globbing characters
Pattern pattern = UserSearchUtils.createContainsPattern(searchText, true,
Pattern.DOTALL | Pattern.CASE_INSENSITIVE);
List<ScriptInfo> results = new ArrayList<>();
for (ScriptInfo info : data) {
String name = info.getName();
Matcher m = pattern.matcher(name);
if (m.matches()) {
results.add(info);
}
}
return results;
} }
} }
private class ScriptSelectionTextField extends DropDownSelectionTextField<ScriptInfo> { private class ScriptSelectionTextField extends DropDownSelectionTextField<ScriptInfo> {
@@ -18,14 +18,18 @@ package ghidra.app.util.datatype;
import java.awt.*; import java.awt.*;
import java.awt.event.*; import java.awt.event.*;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.help.UnsupportedOperationException;
import javax.swing.*; import javax.swing.*;
import javax.swing.border.Border; import javax.swing.border.Border;
import javax.swing.event.*; import javax.swing.event.*;
import javax.swing.tree.TreePath; import javax.swing.tree.TreePath;
import org.apache.commons.lang3.StringUtils;
import docking.widgets.DropDownSelectionTextField; import docking.widgets.DropDownSelectionTextField;
import docking.widgets.DropDownTextFieldDataModel; import docking.widgets.DropDownTextFieldDataModel;
import docking.widgets.button.BrowseButton; import docking.widgets.button.BrowseButton;
@@ -406,16 +410,37 @@ public class CategoryPathSelectionEditor extends AbstractCellEditor {
return categoryPath.getPath(); return categoryPath.getPath();
} }
@Override
public List<SearchMode> getSupportedSearchModes() {
return List.of(SearchMode.CONTAINS, SearchMode.STARTS_WITH, SearchMode.WILDCARD);
}
@Override @Override
public List<CategoryPath> getMatchingData(String searchText) { public List<CategoryPath> getMatchingData(String searchText) {
if (searchText == null || searchText.length() == 0) { throw new UnsupportedOperationException(
return Collections.emptyList(); "Method no longer supported. Instead, call getMatchingData(String, SearchMode)");
}
@Override
public List<CategoryPath> getMatchingData(String searchText, SearchMode mode) {
if (StringUtils.isBlank(searchText)) {
return new ArrayList<>(data);
} }
if (!getSupportedSearchModes().contains(mode)) {
throw new IllegalArgumentException("Unsupported SearchMode: " + mode);
}
Pattern p = mode.createPattern(searchText);
return getMatchingDataRegex(p);
}
private List<CategoryPath> getMatchingDataRegex(Pattern p) {
List<CategoryPath> results = new ArrayList<>(); List<CategoryPath> results = new ArrayList<>();
for (CategoryPath path : data) { for (CategoryPath path : data) {
String pathString = path.getPath(); String pathString = path.getPath();
if (pathString.contains(searchText)) { Matcher m = p.matcher(pathString);
if (m.matches()) {
results.add(path); results.add(path);
} }
} }
@@ -17,7 +17,10 @@ package ghidra.app.util.datatype;
import java.awt.Component; import java.awt.Component;
import java.util.*; import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.help.UnsupportedOperationException;
import javax.swing.*; import javax.swing.*;
import docking.widgets.DropDownSelectionTextField; import docking.widgets.DropDownSelectionTextField;
@@ -69,6 +72,11 @@ public class DataTypeDropDownSelectionDataModel implements DropDownTextFieldData
return service; return service;
} }
@Override
public List<SearchMode> getSupportedSearchModes() {
return List.of(SearchMode.STARTS_WITH, SearchMode.CONTAINS, SearchMode.WILDCARD);
}
@Override @Override
public ListCellRenderer<DataType> getListRenderer() { public ListCellRenderer<DataType> getListRenderer() {
return new DataTypeDropDownRenderer(); return new DataTypeDropDownRenderer();
@@ -86,13 +94,47 @@ public class DataTypeDropDownSelectionDataModel implements DropDownTextFieldData
@Override @Override
public List<DataType> getMatchingData(String searchText) { public List<DataType> getMatchingData(String searchText) {
throw new UnsupportedOperationException(
"Method no longer supported. Instead, call getMatchingData(String, SearchMode)");
}
@Override
public List<DataType> getMatchingData(String searchText, SearchMode mode) {
if (searchText == null || searchText.length() == 0) { if (searchText == null || searchText.length() == 0) {
// full list results not supported since the data may be too large for user interaction
return Collections.emptyList(); return Collections.emptyList();
} }
List<DataType> dataTypeList = if (!getSupportedSearchModes().contains(mode)) {
throw new IllegalArgumentException("Unsupported SearchMode: " + mode);
}
if (mode == SearchMode.STARTS_WITH) {
return getMatchDataStartsWith(searchText);
}
Pattern p = mode.createPattern(searchText);
return getMatchingDataRegex(p);
}
private List<DataType> getMatchDataStartsWith(String searchText) {
List<DataType> results =
DataTypeUtils.getStartsWithMatchingDataTypes(searchText, dataTypeService); DataTypeUtils.getStartsWithMatchingDataTypes(searchText, dataTypeService);
return filterDataTypeList(dataTypeList); return filterDataTypeList(results);
}
private List<DataType> getMatchingDataRegex(Pattern p) {
List<DataType> results = new ArrayList<>();
List<DataType> allTypes = dataTypeService.getSortedDataTypeList();
for (DataType dt : allTypes) {
String name = dt.getName().toLowerCase();
Matcher m = p.matcher(name);
if (m.matches()) {
results.add(dt);
}
}
return filterDataTypeList(results);
} }
/** /**
@@ -151,6 +151,8 @@ public class DataTypeSelectionEditor extends AbstractCellEditor {
editorPanel.add(selectionField); editorPanel.add(selectionField);
editorPanel.add(browsePanel); editorPanel.add(browsePanel);
// This listener is not installed under certain conditions, such as when
// setTabCommitsEdit(true) is called.
keyListener = new KeyAdapter() { keyListener = new KeyAdapter() {
@Override @Override
@@ -87,7 +87,7 @@ import resources.ResourceManager;
public abstract class AbstractScreenShotGenerator extends AbstractGhidraHeadedIntegrationTest { 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 { static {
System.setProperty("user.name", "User-1"); System.setProperty("user.name", "User-1");
@@ -787,7 +787,7 @@ public abstract class AbstractFunctionGraphTest extends AbstractGhidraHeadedInte
waitForSwing(); waitForSwing();
int tryCount = 3; int tryCount = 0;
while (tryCount++ < 5 && updater.isBusy()) { while (tryCount++ < 5 && updater.isBusy()) {
waitForConditionWithoutFailing(() -> !updater.isBusy()); waitForConditionWithoutFailing(() -> !updater.isBusy());
} }
@@ -72,6 +72,7 @@ public class HelpManager implements HelpService {
private HashMap<URL, HelpSet> urlToHelpSets = new HashMap<>(); private HashMap<URL, HelpSet> urlToHelpSets = new HashMap<>();
private Map<Object, HelpLocation> helpLocations = new WeakHashMap<>(); private Map<Object, HelpLocation> helpLocations = new WeakHashMap<>();
private Map<Object, DynamicHelpLocation> dynamicHelp = new WeakHashMap<>();
private List<HelpSet> helpSetsPendingMerge = new ArrayList<>(); private List<HelpSet> helpSetsPendingMerge = new ArrayList<>();
private boolean hasMergedHelpSets; private boolean hasMergedHelpSets;
@@ -137,6 +138,14 @@ public class HelpManager implements HelpService {
return HOME_ID; 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 @Override
public void excludeFromHelp(Object helpObject) { public void excludeFromHelp(Object helpObject) {
excludedFromHelp.add(helpObject); excludedFromHelp.add(helpObject);
@@ -153,6 +162,11 @@ public class HelpManager implements HelpService {
helpLocations.remove(helpObject); helpLocations.remove(helpObject);
} }
@Override
public void registerDynamicHelp(Object helpObject, DynamicHelpLocation helpLocation) {
dynamicHelp.put(helpObject, helpLocation);
}
@Override @Override
public void registerHelp(Object helpObject, HelpLocation location) { public void registerHelp(Object helpObject, HelpLocation location) {
@@ -197,15 +211,29 @@ public class HelpManager implements HelpService {
@Override @Override
public HelpLocation getHelpLocation(Object helpObj) { 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); return helpLocations.get(helpObj);
} }
/** private HelpLocation findHelpLocation(Object helpObj) {
* Returns the master help set (the one into which all other help sets are merged). if (helpObj instanceof HelpDescriptor) {
* @return the help set HelpDescriptor helpDescriptor = (HelpDescriptor) helpObj;
*/ Object descriptorHelpObj = helpDescriptor.getHelpObject();
public GHelpSet getMasterHelpSet() { return doGetHelpLocation(descriptorHelpObj);
return mainHS; }
return doGetHelpLocation(helpObj);
} }
@Override @Override
@@ -347,15 +375,6 @@ public class HelpManager implements HelpService {
throw helpException; 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) { private String getFilenameForHelpLocation(HelpLocation helpLocation) {
URL helpFileURL = getURLForHelpLocation(helpLocation); URL helpFileURL = getURLForHelpLocation(helpLocation);
if (helpFileURL == null) { if (helpFileURL == null) {
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 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.theme.GThemeDefaults.Colors.Palette;
import generic.util.image.ImageUtils; import generic.util.image.ImageUtils;
import generic.util.image.ImageUtils.Padding;
import ghidra.util.Msg;
public class Callout { 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; private static final int CALLOUT_BORDER_PADDING = 20;
public Image createCallout(CalloutComponentInfo calloutInfo) { public Image createCalloutOnImage(Image image, CalloutInfo calloutInfo) {
try {
double distanceFactor = 1.15; return doCreateCalloutOnImage(image, calloutInfo);
}
// catch (Exception e) {
// Callout Size Msg.error(this, "Unexpected exception creating callout image", e);
// throw e;
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
} }
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 // 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(); Rectangle clientBounds = calloutInfo.getBounds();
int newHeight = cSize.height * 6; Dimension clientShapeSize = clientBounds.getSize();
int newHeight = clientShapeSize.height * 6;
int calloutHeight = newHeight; int calloutHeight = newHeight;
int calloutWidth = calloutHeight; // square int calloutWidth = calloutHeight; // square
// //
// Callout Distance (from original component). This is the location (relative to // Callout Offset (from original shape that is being magnified). This is the location
// the original component) of the callout image (not the full shape). So, if the // (relative to the original component) of the callout image (not the full shape; the round
// x distance was 10, then the callout image would start 10 pixels to the right of // magnified image). So, if the x offset is 10, then the callout image would start 10 pixels
// the component. // to the right of the component.
// //
double distanceX = calloutWidth * 1.5; double offsetX = calloutWidth * 1.5;
double distanceY = calloutHeight * 2; double offsetY = calloutHeight * 2;
// only pad if the callout leaves the bounds of the parent image // only pad if the callout leaves the bounds of the parent image
int topPadding = 0; int topPadding = 0;
Rectangle componentBounds = calloutInfo.getBounds(); Point clientLocation = clientBounds.getLocation();
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);
}
// //
// Callout Bounds // Callout Bounds
// //
// angle the callout // set the callout location offset from the client area and angle it as well
double theta = Math.toRadians(45); double theta = Math.toRadians(45);
int calloutX = (int) (componentLocation.x + (Math.cos(theta) * distanceX)); int calloutX = (int) (clientLocation.x + (Math.cos(theta) * offsetX));
int calloutY = (int) (componentLocation.y - (Math.sin(theta) * distanceY)); int calloutY = (int) (clientLocation.y - (Math.sin(theta) * offsetY));
Rectangle calloutShapeBounds =
int backgroundWidth = calloutWidth; new Rectangle(calloutX, calloutY, calloutWidth, calloutHeight);
int backgroundHeight = backgroundWidth; // square
Rectangle calloutBounds =
new Rectangle(calloutX, calloutY, backgroundWidth, backgroundHeight);
// //
// Full Callout Shape Bounds (this does not include the drop-shadow) // Full Callout Shape Bounds (this does not include the drop-shadow)
// //
Rectangle calloutDrawingArea = componentBounds.union(calloutBounds); Rectangle calloutBounds = clientBounds.union(calloutShapeBounds);
BufferedImage calloutImage = 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(); DropShadow dropShadow = new DropShadow();
Image shadow = dropShadow.createDropShadow(calloutImage, 40); Image shadow = dropShadow.createDropShadow(calloutImage, 40);
// //
// Create our final image and draw into it the callout image and its shadow // 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, Padding padding = createImagePadding(image, shadow, calloutBounds, topPadding);
shadow.getWidth(null), shadow.getHeight(null)); Color bg = Palette.WHITE;
Rectangle completeBounds = calloutDrawingArea.union(dropShadowBounds); Image paddedImage = ImageUtils.padImage(image, bg, padding);
int fullBoundsXEndpoint = calloutImageLoc.x + completeBounds.width; Graphics g = paddedImage.getGraphics();
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();
Graphics2D g2d = (Graphics2D) g; Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.drawImage(shadow, calloutImageLoc.x, calloutImageLoc.y, null); // Get the final location that may have been updated if we padded the image
g2d.drawImage(calloutImage, calloutImageLoc.x, calloutImageLoc.y, null); 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 // Debug
// //
// g2d.setColor(Palette.RED); // 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.setColor(Palette.ORANGE);
// g2d.draw(calloutBounds); // Rectangle destCalloutBounds = new Rectangle(calloutShapeBounds);
// calloutInfo.moveToImage(destCalloutBounds, padding);
// destCalloutBounds.setLocation(destCalloutBounds.getLocation());
// g2d.draw(destCalloutBounds);
// //
// g2d.setColor(Palette.BLUE); // g2d.setColor(Palette.BLUE);
// g2d.draw(componentBounds); // Rectangle movedClient = new Rectangle(calloutInfo.getBounds());
// // calloutInfo.moveToImage(movedClient, padding);
// g2d.setColor(Palette.MAGENTA); // g2d.draw(movedClient);
// 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);
return image; return paddedImage;
} }
private BufferedImage createCalloutImage(CalloutComponentInfo calloutInfo, Point cLoc, private Padding createImagePadding(Image fullImage, Image shadow, Rectangle calloutOnlyBounds,
Rectangle calloutBounds, Rectangle fullBounds) { 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 = BufferedImage calloutImage =
new BufferedImage(fullBounds.width, fullBounds.height, BufferedImage.TYPE_INT_ARGB); new BufferedImage(fullBounds.width, fullBounds.height, BufferedImage.TYPE_INT_ARGB);
Graphics2D cg = (Graphics2D) calloutImage.getGraphics(); Graphics2D cg = (Graphics2D) calloutImage.getGraphics();
@@ -270,30 +187,33 @@ public class Callout {
// Make relative our two shapes--the component shape and the callout shape // Make relative our two shapes--the component shape and the callout shape
// //
Point calloutOrigin = fullBounds.getLocation(); // the shape is relative to the full bounds Point calloutOrigin = fullBounds.getLocation(); // the shape is relative to the full bounds
int sx = calloutBounds.x - calloutOrigin.x; int sx = calloutShapeBounds.x - calloutOrigin.x;
int sy = calloutBounds.y - calloutOrigin.y; int sy = calloutShapeBounds.y - calloutOrigin.y;
Ellipse2D calloutShape =
new Ellipse2D.Double(sx, sy, calloutBounds.width, calloutBounds.height);
int cx = cLoc.x - calloutOrigin.x; Ellipse2D calloutShape =
int cy = cLoc.y - calloutOrigin.y; new Ellipse2D.Double(sx, sy, calloutShapeBounds.width, calloutShapeBounds.height);
Dimension cSize = calloutInfo.getSize();
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 // TODO this shows how to correctly account for scaling in the Function Graph
// Dimension cSize2 = new Dimension(cSize); // Dimension cSize2 = new Dimension(cSize);
// double scale = .5d; // double scale = .5d;
// cSize2.width *= scale; // cSize2.width *= scale;
// cSize2.height *= 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); paintCalloutCircularImage(cg, calloutInfo, calloutShape);
cg.dispose(); cg.dispose();
return calloutImage; return calloutImage;
} }
private void paintCalloutCircularImage(Graphics2D g, CalloutComponentInfo calloutInfo, private void paintCalloutCircularImage(Graphics2D g, CalloutInfo calloutInfo,
RectangularShape shape) { RectangularShape shape) {
// //
@@ -325,8 +245,8 @@ public class Callout {
g.drawImage(foregroundImage, ir.x, ir.y, null); g.drawImage(foregroundImage, ir.x, ir.y, null);
} }
private void paintCalloutArrow(Graphics2D g2d, RectangularShape componentShape, private void paintCalloutArrow(Graphics2D g2d, Rectangle componentShape,
RectangularShape calloutShape) { Rectangle calloutShape) {
Rectangle cr = componentShape.getBounds(); Rectangle cr = componentShape.getBounds();
Rectangle sr = calloutShape.getBounds(); Rectangle sr = calloutShape.getBounds();
@@ -362,12 +282,10 @@ public class Callout {
} }
private Image createMagnifiedImage(GraphicsConfiguration gc, Dimension imageSize, private Image createMagnifiedImage(GraphicsConfiguration gc, Dimension imageSize,
CalloutComponentInfo calloutInfo, RectangularShape imageShape) { CalloutInfo calloutInfo, RectangularShape imageShape) {
Dimension componentSize = calloutInfo.getSize(); Rectangle r = new Rectangle(calloutInfo.getBounds());
Point componentScreenLocation = calloutInfo.getLocationOnScreen(); calloutInfo.moveToScreen(r);
Rectangle r = new Rectangle(componentScreenLocation, componentSize);
int offset = 100; int offset = 100;
r.x -= offset; r.x -= offset;
@@ -381,7 +299,8 @@ public class Callout {
compImage = robot.createScreenCapture(r); compImage = robot.createScreenCapture(r);
} }
catch (AWTException e) { 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(); double magnification = calloutInfo.getMagnification();
@@ -1,99 +0,0 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking.util.image;
import java.awt.*;
import javax.swing.SwingUtilities;
/**
* An object that describes a component to be 'called-out'. A callout is a way to
* emphasize a widget (usually this is only needed for small GUI elements, like an action or
* icon).
*
* <P>The given component info is used to render a magnified image of the given component
* onto another image. For this to work, the rendering engine will need to know how to
* translate the component's location to that of the image space onto which the callout
* will be drawn. This is the purpose of requiring the 'destination component'. That
* component provides the bounds that will be used to move the component's relative position
* (which is relative to the components parent).
*/
public class CalloutComponentInfo {
Point locationOnScreen;
Point relativeLocation;
Dimension size;
Component component;
Component destinationComponent;
double magnification = 2.0;
public CalloutComponentInfo(Component destinationComponent, Component component) {
this(destinationComponent, component, component.getLocationOnScreen(),
component.getLocation(), component.getSize());
}
public CalloutComponentInfo(Component destinationComponent, Component component,
Point locationOnScreen, Point relativeLocation, Dimension size) {
this.destinationComponent = destinationComponent;
this.component = component;
this.locationOnScreen = locationOnScreen;
this.relativeLocation = relativeLocation;
this.size = size;
}
public Point convertPointToParent(Point location) {
return SwingUtilities.convertPoint(component.getParent(), location, destinationComponent);
}
public void setMagnification(double magnification) {
this.magnification = magnification;
}
Component getComponent() {
return component;
}
/**
* Returns the on-screen location of the component. This is used for screen capture, which
* means if you move the component after this info has been created, this location will
* be outdated.
*
* @return the location
*/
Point getLocationOnScreen() {
return locationOnScreen;
}
/**
* The size of the component we will be calling out
*
* @return the size
*/
Dimension getSize() {
return size;
}
Rectangle getBounds() {
return new Rectangle(relativeLocation, size);
}
double getMagnification() {
return magnification;
}
}
@@ -0,0 +1,125 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking.util.image;
import java.awt.*;
import javax.swing.SwingUtilities;
import generic.util.image.ImageUtils.Padding;
/**
* An object that describes a component to be 'called-out'. A callout is a way to
* emphasize a widget (usually this is only needed for small GUI elements, like an action or
* icon).
*
* <P>The given component info is used to render a magnified image of the given component
* onto another image. For this to work, the rendering engine will need to know how to
* translate the component's location to that of the image space onto which the callout
* will be drawn. This is the purpose of requiring the 'destination component'. That
* component provides the bounds that will be used to move the component's relative position
* (which is relative to the components parent).
*/
public class CalloutInfo {
private Rectangle clientShape;
private Component source;
private Component destination;
private double magnification = 2.0;
/**
* Constructor for the destination component, the source component and the area that is to be
* captured. This constructor will call out the entire shape of the given source component.
* <p>
* The destination component needs to be the item that was captured in the screenshot. If you
* captured a window, then pass that window as the destination. If you captured a sub-component
* of a window, then pass that sub-component as the destination.
*
* @param destinationComponent the component over which the image will be painted
* @param sourceComponent the component that contains the area that will be called out
*/
public CalloutInfo(Component destinationComponent, Component sourceComponent) {
this(destinationComponent, sourceComponent, sourceComponent.getBounds());
}
/**
* Constructor for the destination component, the source component and the area that is to be
* captured.
* <p>
* The destination component needs to be the item that was captured in the screenshot. If you
* captured a window, then pass that window as the destination. If you captured a sub-component
* of a window, then pass that sub-component as the destination.
*
* @param destinationComponent the component over which the image will be painted
* @param sourceComponent the component that contains the area that will be called out
* @param clientShape the shape that will be called out
*/
public CalloutInfo(Component destinationComponent, Component sourceComponent,
Rectangle clientShape) {
this.destination = destinationComponent;
this.source = sourceComponent;
this.clientShape = clientShape;
}
public void setMagnification(double magnification) {
this.magnification = magnification;
}
public double getMagnification() {
return magnification;
}
/**
* Moves the given rectangle to the image destination space. Clients use this to create new
* shapes using the <B>client space</B> and then move them to the image destination space.
* @param r the rectangle
* @param padding any padding around the destination image
*/
public void moveToImage(Rectangle r, Padding padding) {
moveToDestination(r);
r.x += padding.left();
r.y += padding.top();
}
/**
* Moves the given rectangle to the image destination space. Clients use this to create new
* shapes using the <B>client space</B>. This destination space is not the same as the final
* image that will get created.
* @param r the rectangle
*/
public void moveToDestination(Rectangle r) {
Point oldPoint = r.getLocation();
Point newPoint = SwingUtilities.convertPoint(source.getParent(), oldPoint, destination);
r.setLocation(newPoint);
}
/**
* Moves the given rectangle to screen space. Clients use this to create new shapes using the
* <B>client space</B> and then move them to the image destination space.
* @param r the rectangle
*/
public void moveToScreen(Rectangle r) {
Point p = r.getLocation();
SwingUtilities.convertPointToScreen(p, source.getParent());
r.setLocation(p);
}
public Rectangle getBounds() {
return new Rectangle(clientShape);
}
}
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 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.event.MouseEvent;
import java.awt.image.*; import java.awt.image.*;
import javax.swing.JFrame; import javax.swing.*;
import javax.swing.JPanel;
import generic.theme.GThemeDefaults.Colors.Palette; import generic.theme.GThemeDefaults.Colors.Palette;
@@ -30,6 +29,103 @@ public class DropShadow {
private Color shadowColor = Palette.BLACK; private Color shadowColor = Palette.BLACK;
private float shadowOpacity = 0.85f; 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) { public static void main(String[] args) {
final DropShadow ds = new DropShadow(); final DropShadow ds = new DropShadow();
@@ -102,148 +198,9 @@ public class DropShadow {
canvas.repaint(); canvas.repaint();
} }
}); });
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
frame.setVisible(true); frame.setVisible(true);
frame.pack(); 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));
// }
} }
@@ -16,9 +16,14 @@
package docking.widgets; package docking.widgets;
import java.util.*; import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.help.UnsupportedOperationException;
import javax.swing.ListCellRenderer; import javax.swing.ListCellRenderer;
import org.apache.commons.lang3.StringUtils;
import docking.widgets.list.GListCellRenderer; import docking.widgets.list.GListCellRenderer;
import ghidra.util.datastruct.CaseInsensitiveDuplicateStringComparator; import ghidra.util.datastruct.CaseInsensitiveDuplicateStringComparator;
@@ -53,8 +58,48 @@ public class DefaultDropDownSelectionDataModel<T> implements DropDownTextFieldDa
Collections.sort(data, comparator); Collections.sort(data, comparator);
} }
@Override
public List<SearchMode> getSupportedSearchModes() {
return List.of(SearchMode.STARTS_WITH, SearchMode.CONTAINS, SearchMode.WILDCARD);
}
@Override @Override
public List<T> getMatchingData(String searchText) { public List<T> getMatchingData(String searchText) {
throw new UnsupportedOperationException(
"Method no longer supported. Instead, call getMatchingData(String, SearchMode)");
}
@Override
public List<T> getMatchingData(String searchText, SearchMode mode) {
if (StringUtils.isBlank(searchText)) {
return new ArrayList<>(data);
}
if (!getSupportedSearchModes().contains(mode)) {
throw new IllegalArgumentException("Unsupported SearchMode: " + mode);
}
if (mode == SearchMode.STARTS_WITH) {
return getMatchingDataStartsWith(searchText);
}
Pattern p = mode.createPattern(searchText);
return getMatchingDataRegex(p);
}
private List<T> getMatchingDataRegex(Pattern p) {
List<T> results = new ArrayList<>();
for (T t : data) {
String string = searchConverter.getString(t);
Matcher m = p.matcher(string);
if (m.matches()) {
results.add(t);
}
}
return results;
}
private List<T> getMatchingDataStartsWith(String searchText) {
List<?> l = data; List<?> l = data;
int startIndex = Collections.binarySearch(l, (Object) searchText, comparator); int startIndex = Collections.binarySearch(l, (Object) searchText, comparator);
int endIndex = Collections.binarySearch(l, (Object) (searchText + END_CHAR), comparator); int endIndex = Collections.binarySearch(l, (Object) (searchText + END_CHAR), comparator);
File diff suppressed because it is too large Load Diff
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -15,10 +15,16 @@
*/ */
package docking.widgets; 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 javax.swing.ListCellRenderer;
import ghidra.util.UserSearchUtils;
/** /**
* This interface represents all methods needed by the {@link DropDownSelectionTextField} in order * This interface represents all methods needed by the {@link DropDownSelectionTextField} in order
* to search, show, manipulate and select objects. * to search, show, manipulate and select objects.
@@ -27,15 +33,112 @@ import javax.swing.ListCellRenderer;
*/ */
public interface DropDownTextFieldDataModel<T> { public interface DropDownTextFieldDataModel<T> {
public enum SearchMode {
/** Matches when any line of data contains the search text */
CONTAINS("()", "Contains"),
/** Matches when any line of data starts with the search text */
STARTS_WITH("^", "Starts With"),
/** Matches when any line of data contains the search text using globbing characters */
WILDCARD("*?", "Wildcard"),
/** Used internally */
UNKNOWN("", "");
private String hint;
private String displayName;
SearchMode(String hint, String displayName) {
this.hint = hint;
this.displayName = displayName;
}
public String getHint() {
return hint;
}
public String getDisplayName() {
return displayName;
}
/**
* Creates search pattern for the given input text. Clients do not have to use this method
* and a free to create their own text matching mechanism.
* @param input the input for which to search
* @return the pattern
* @see UserSearchUtils
*/
public Pattern createPattern(String input) {
switch (this) {
case CONTAINS:
return createContainsPattern(input, false, Pattern.CASE_INSENSITIVE);
case STARTS_WITH:
return createStartsWithPattern(input, false, Pattern.CASE_INSENSITIVE);
case WILDCARD:
return createSearchPattern(input, false);
default:
throw new IllegalStateException("Cannot create pattern for mode: " + this);
}
}
}
/** /**
* Returns a list of data that matches the given <code>searchText</code>. A match typically * Returns a list of data that matches the given <code>searchText</code>. A list is returned to
* means a "startsWith" match. A list is returned to allow for multiple matches. * allow for multiple matches. The type of matching performed is determined by the current
* {@link #getSupportedSearchModes() search mode}. If the implementation of this model does not
* support search modes, then it is up the the implementor to determine how matches are found.
* <P>
* Implementation Note: a client request for all data will happen using the empty string. If
* your data model is sufficiently large, then you may choose to not return any data in this
* case. Smaller data sets should return all data when given the empty string
* *
* @param searchText The text used to find matches. * @param searchText The text used to find matches.
* @return a list of items matching the given text. * @return a list of items matching the given text.
* @see #getMatchingData(String, SearchMode)
*/ */
public List<T> getMatchingData(String searchText); public List<T> getMatchingData(String searchText);
/**
* Returns a list of data that matches the given <code>searchText</code>. A list is returned to
* allow for multiple matches. The type of matching performed is determined by the current
* {@link #getSupportedSearchModes() search mode}. If the implementation of this model does not
* support search modes, then it is up the the implementor to determine how matches are found.
* <P>
* Implementation Note: a client request for all data will happen using the empty string. If
* your data model is sufficiently large, then you may choose to not return any data in this
* case. Smaller data sets should return all data when given the empty string
*
* @param searchText the text used to find matches.
* @param searchMode the search mode to use
* @return a list of items matching the given text.
* @throws IllegalArgumentException if the given search mode is not supported
* @see #getMatchingData(String, SearchMode)
*/
public default List<T> getMatchingData(String searchText, SearchMode searchMode) {
// Clients that override getSupportedSearchModes() must also override this method to perform
// the correct type of search
if (searchMode != SearchMode.UNKNOWN) {
throw new UnsupportedOperationException(
"You must override this method to use search modes");
}
// Use the default matching data
return getMatchingData(searchText);
}
/**
* Subclasses can override this to return all supported search modes. The order of the modes is
* the order which they will cycle when requested by the user. The first mode is the default
* search mode.
* @return the supported search modes
*/
public default List<SearchMode> getSupportedSearchModes() {
return List.of(SearchMode.UNKNOWN);
}
/** /**
* Returns the index in the given list of the first item that matches the given text. For * 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 * data sets that do not allow duplicates, this is simply the index of the item that matches
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 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.awt.Component;
import java.io.File; import java.io.File;
import java.util.*; import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.help.UnsupportedOperationException;
import javax.swing.*; import javax.swing.*;
import javax.swing.filechooser.FileSystemView; import javax.swing.filechooser.FileSystemView;
import org.apache.commons.lang3.StringUtils;
import docking.widgets.DropDownSelectionTextField; import docking.widgets.DropDownSelectionTextField;
import docking.widgets.DropDownTextFieldDataModel; import docking.widgets.DropDownTextFieldDataModel;
import docking.widgets.list.GListCellRenderer; import docking.widgets.list.GListCellRenderer;
@@ -84,12 +89,58 @@ public class FileDropDownSelectionDataModel implements DropDownTextFieldDataMode
return new FileDropDownRenderer(); return new FileDropDownRenderer();
} }
@Override
public List<SearchMode> getSupportedSearchModes() {
return List.of(SearchMode.STARTS_WITH, SearchMode.CONTAINS, SearchMode.WILDCARD);
}
@Override @Override
public List<File> getMatchingData(String searchText) { public List<File> getMatchingData(String searchText) {
if (searchText == null || searchText.length() == 0) { throw new UnsupportedOperationException(
"Method no longer supported. Instead, call getMatchingData(String, SearchMode)");
}
@Override
public List<File> getMatchingData(String searchText, SearchMode mode) {
if (StringUtils.isBlank(searchText)) {
// full data display not support, as we don't know how big the data may be
return Collections.emptyList(); return Collections.emptyList();
} }
if (!getSupportedSearchModes().contains(mode)) {
throw new IllegalArgumentException("Unsupported SearchMode: " + mode);
}
if (mode == SearchMode.STARTS_WITH) {
return getMatchDataStartsWith(searchText);
}
Pattern p = mode.createPattern(searchText);
return getMatchingDataRegex(p);
}
private List<File> getMatchingDataRegex(Pattern p) {
List<File> matches = new ArrayList<>();
List<File> list = getSortedFiles();
for (File file : list) {
String name = file.getName();
Matcher m = p.matcher(name);
if (m.matches()) {
matches.add(file);
}
}
return matches;
}
private List<File> getMatchDataStartsWith(String searchText) {
List<File> list = getSortedFiles();
return getMatchingSubList(searchText, searchText + END_CHAR, list);
}
private List<File> getSortedFiles() {
File directory = chooser.getCurrentDirectory(); File directory = chooser.getCurrentDirectory();
File[] files = directory.listFiles(); File[] files = directory.listFiles();
if (files == null) { if (files == null) {
@@ -101,8 +152,7 @@ public class FileDropDownSelectionDataModel implements DropDownTextFieldDataMode
} }
Collections.sort(list, sortComparator); Collections.sort(list, sortComparator);
return list;
return getMatchingSubList(searchText, searchText + END_CHAR, list);
} }
private List<File> getMatchingSubList(String searchTextStart, String searchTextEnd, private List<File> getMatchingSubList(String searchTextStart, String searchTextEnd,
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -138,9 +138,15 @@ public class AutocompletingStringConstraintEditor extends DataLoadingConstraintE
@Override @Override
public List<String> getMatchingData(String searchText) { public List<String> getMatchingData(String searchText) {
if (StringUtils.isBlank(searchText) || !isValidPatternString(searchText)) { if (!isValidPatternString(searchText)) {
return Collections.emptyList(); 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(); searchText = searchText.trim();
lastConstraint = (StringColumnConstraint) currentConstraint lastConstraint = (StringColumnConstraint) currentConstraint
.parseConstraintValue(searchText, columnDataSource.getTableDataSource()); .parseConstraintValue(searchText, columnDataSource.getTableDataSource());
@@ -19,8 +19,7 @@ import static org.junit.Assert.*;
import java.awt.BorderLayout; import java.awt.BorderLayout;
import java.awt.event.*; import java.awt.event.*;
import java.util.Arrays; import java.util.*;
import java.util.List;
import javax.swing.*; import javax.swing.*;
import javax.swing.event.CellEditorListener; import javax.swing.event.CellEditorListener;
@@ -30,6 +29,7 @@ import org.junit.After;
import org.junit.Before; import org.junit.Before;
import docking.test.AbstractDockingTest; import docking.test.AbstractDockingTest;
import docking.widgets.DropDownTextFieldDataModel.SearchMode;
public abstract class AbstractDropDownTextFieldTest<T> extends AbstractDockingTest { public abstract class AbstractDropDownTextFieldTest<T> extends AbstractDockingTest {
@@ -151,21 +151,30 @@ public abstract class AbstractDropDownTextFieldTest<T> extends AbstractDockingTe
return item; 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) { protected void assertSelectedListItem(int expected) {
JList<T> list = textField.getJList(); JList<T> list = textField.getJList();
int actual = runSwing(() -> list.getSelectedIndex()); int actual = runSwing(() -> list.getSelectedIndex());
assertEquals(expected, actual); 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) { protected void assertSelectedListItem(T expected) {
JList<T> list = textField.getJList(); JList<T> list = textField.getJList();
T actual = runSwing(() -> list.getSelectedValue()); T actual = runSwing(() -> list.getSelectedValue());
assertEquals(expected, actual); 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) { protected void assertSelectedValue(T expected) {
T actual = runSwing(() -> textField.getSelectedValue()); T actual = runSwing(() -> textField.getSelectedValue());
assertEquals(expected, actual); assertEquals(expected, actual);
@@ -177,6 +186,24 @@ public abstract class AbstractDropDownTextFieldTest<T> extends AbstractDockingTe
assertNull(actual); assertNull(actual);
} }
protected void assertMatchesInList(String... expected) {
waitForSwing();
assertMatchingWindowShowing();
@SuppressWarnings("unchecked")
JList<String> list = (JList<String>) textField.getJList();
ListModel<String> model = list.getModel();
int n = model.getSize();
assertEquals("Expected item size is not the same as the matching list size",
expected.length, n);
HashSet<String> set = new HashSet<>(Arrays.asList(expected));
for (int i = 0; i < n; i++) {
String item = model.getElementAt(i);
assertTrue("Item in list not expected: " + item, set.contains(item));
}
}
protected void assertNoEditingCancelledEvent() { protected void assertNoEditingCancelledEvent() {
assertEquals("Received unexpected editingCanceled() invocations.", listener.canceledCount, assertEquals("Received unexpected editingCanceled() invocations.", listener.canceledCount,
0); 0);
@@ -252,6 +279,15 @@ public abstract class AbstractDropDownTextFieldTest<T> extends AbstractDockingTe
runSwing(() -> textField.setText(text)); 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() { protected void closeMatchingWindow() {
JWindow window = runSwing(() -> textField.getActiveMatchingWindow()); JWindow window = runSwing(() -> textField.getActiveMatchingWindow());
if (window == null) { if (window == null) {
@@ -294,6 +330,16 @@ public abstract class AbstractDropDownTextFieldTest<T> extends AbstractDockingTe
waitForSwing(); 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) { protected void typeText(final String text, boolean expectWindow) {
waitForSwing(); waitForSwing();
triggerText(textField, text); triggerText(textField, text);
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -15,7 +15,7 @@
*/ */
package docking.widgets; package docking.widgets;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.*;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@@ -23,6 +23,7 @@ import java.util.List;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import docking.widgets.DropDownTextFieldDataModel.SearchMode;
import generic.test.AbstractGenericTest; import generic.test.AbstractGenericTest;
public class DefaultDropDownSelectionDataModelTest extends AbstractGenericTest { public class DefaultDropDownSelectionDataModelTest extends AbstractGenericTest {
@@ -48,11 +49,11 @@ public class DefaultDropDownSelectionDataModelTest extends AbstractGenericTest {
@Test @Test
public void testGetMatchingData() { public void testGetMatchingData() {
List<TestType> matchingData = model.getMatchingData("a"); List<TestType> matchingData = model.getMatchingData("a", SearchMode.STARTS_WITH);
assertEquals(1, matchingData.size()); assertEquals(1, matchingData.size());
assertEquals("abc", matchingData.get(0).getName()); assertEquals("abc", matchingData.get(0).getName());
matchingData = model.getMatchingData("bac"); matchingData = model.getMatchingData("bac", SearchMode.STARTS_WITH);
assertEquals(2, matchingData.size()); assertEquals(2, matchingData.size());
assertEquals("bac", matchingData.get(0).getName()); assertEquals("bac", matchingData.get(0).getName());
assertEquals("bace", matchingData.get(1).getName()); assertEquals("bace", matchingData.get(1).getName());
@@ -19,14 +19,15 @@ import static org.junit.Assert.*;
import java.awt.Dimension; import java.awt.Dimension;
import java.awt.Point; import java.awt.Point;
import java.awt.event.KeyEvent; import java.awt.event.*;
import java.awt.event.MouseEvent;
import javax.swing.JList; import javax.swing.JList;
import javax.swing.JWindow; import javax.swing.JWindow;
import org.junit.Test; import org.junit.Test;
import docking.widgets.DropDownTextFieldDataModel.SearchMode;
/** /**
* This test achieves partial coverage of {@link DropDownTextField}. Further coverage is * This test achieves partial coverage of {@link DropDownTextField}. Further coverage is
* provided by {@link DropDownSelectionTextFieldTest}, as that test enables item selection * provided by {@link DropDownSelectionTextFieldTest}, as that test enables item selection
@@ -212,10 +213,12 @@ public class DropDownTextFieldTest extends AbstractDropDownTextFieldTest<String>
runSwing(() -> parentFrame.setLocation(p)); runSwing(() -> parentFrame.setLocation(p));
waitForSwing(); waitForSwing();
JWindow currentMatchingWindow = textField.getActiveMatchingWindow(); // we expect the location to change, but there may be a delay
Point newLocation = runSwing(() -> currentMatchingWindow.getLocationOnScreen()); waitForCondition(() -> {
assertNotEquals("The completion window's location did not update when its parent window " + JWindow currentMatchingWindow = textField.getActiveMatchingWindow();
"was moved.", location, newLocation); Point newLocation = runSwing(() -> currentMatchingWindow.getLocationOnScreen());
return !location.equals(newLocation);
});
} }
@Test @Test
@@ -470,6 +473,190 @@ public class DropDownTextFieldTest extends AbstractDropDownTextFieldTest<String>
assertMatchingWindowShowing(); assertMatchingWindowShowing();
} }
@Test
public void testSearchMode_Contains() {
setSearchMode(SearchMode.CONTAINS);
typeText("1", true);
assertMatchesInList("a1", "d1", "e1", "e12", "e123");
clearText();
typeText("e1", true);
assertMatchesInList("e1", "e12", "e123");
clearText();
typeText("z", false);
assertMatchingWindowHidden();
}
@Test
public void testSearchMode_Contains_CaretPositionDoesNotAffectResults() {
setSearchMode(SearchMode.CONTAINS);
typeText("e12", true);
assertMatchesInList("e12", "e123");
left(); // move caret back one position: from e12| to e1|2
assertMatchesInList("e12", "e123");
left(); // move caret back one position: from e1|2 to e|12
assertMatchesInList("e12", "e123");
right(); // move caret back to e1|2
assertMatchesInList("e12", "e123");
right(); // move caret back to e12|
assertMatchesInList("e12", "e123");
}
@Test
public void testSearchMode_StartsWith_CaretPositionChangesResults() {
setSearchMode(SearchMode.STARTS_WITH);
typeText("e12", true);
assertMatchesInList("e12", "e123");
left(); // move caret back one position: from e12| to e1|2
assertMatchesInList("e1", "e12", "e123");
right(); // move caret back to e12|
assertMatchesInList("e12", "e123");
}
@Test
public void testSearchMode_ChangeModeWithText_ToStartsWith_CaretPositionChangesResults() {
/*
The text field honors caret position in 'starts with' mode. Test that changing modes
with text in the field will correctly use the caret position for the given mode.
*/
// start with a search mode that ignores the caret position
setSearchMode(SearchMode.CONTAINS);
typeText("e12", true);
assertMatchesInList("e12", "e123");
left(); // move caret back one position: from e12| to e1|2
assertMatchesInList("e12", "e123"); // same matches in 'contains' mode
setSearchMode(SearchMode.STARTS_WITH);
assertMatchesInList("e1", "e12", "e123"); // caret is at e1|2; matches should change
setSearchMode(SearchMode.CONTAINS);
assertMatchesInList("e12", "e123"); // matches now ignore the caret
}
@Test
public void testSearchMode_Contains_CaretPositionDoesNotChangesResults() {
setSearchMode(SearchMode.CONTAINS);
typeText("e12", true);
assertMatchesInList("e12", "e123");
left(); // move caret back one position: from e12| to e1|2
assertMatchesInList("e12", "e123");
right(); // move caret back to e12|
assertMatchesInList("e12", "e123");
}
@Test
public void testChangeSearchMode_ViaKeyBinding() {
/*
Default search mode order:
STARTS_WITH, CONTAINS, WILDCARD
*/
assertSearchMode(SearchMode.STARTS_WITH);
toggleSearchModeViaKeyBinding();
assertSearchMode(SearchMode.CONTAINS);
toggleSearchModeViaKeyBinding();
assertSearchMode(SearchMode.WILDCARD);
toggleSearchModeViaKeyBinding();
assertSearchMode(SearchMode.STARTS_WITH);
toggleSearchModeViaKeyBinding_Backwards();
assertSearchMode(SearchMode.WILDCARD);
toggleSearchModeViaKeyBinding_Backwards();
assertSearchMode(SearchMode.CONTAINS);
}
@Test
public void testChangeSearchMode_ViaMouse() {
/*
Default search mode order:
STARTS_WITH, CONTAINS, WILDCARD
*/
assertSearchMode(SearchMode.STARTS_WITH);
toggleSearchModeViaMouseClick();
assertSearchMode(SearchMode.CONTAINS);
toggleSearchModeViaMouseClick();
assertSearchMode(SearchMode.WILDCARD);
toggleSearchModeViaMouseClick();
assertSearchMode(SearchMode.STARTS_WITH);
toggleSearchModeViaMouseClick_Backwards();
assertSearchMode(SearchMode.WILDCARD);
toggleSearchModeViaMouseClick_Backwards();
assertSearchMode(SearchMode.CONTAINS);
}
private void toggleSearchModeViaMouseClick() {
clickSearchMode(false);
}
private void toggleSearchModeViaMouseClick_Backwards() {
clickSearchMode(true);
}
private void clickSearchMode(boolean useControlKey) {
// we have to wait, since the bounds are set when the text field paints
DropDownTextField<String>.SearchModeBounds searchModeBounds = waitFor(() -> {
return runSwing(() -> textField.getSearchModeBounds());
});
// this point is relative to the text field
Point p = searchModeBounds.getLocation();
long when = System.currentTimeMillis();
int mods = useControlKey ? InputEvent.CTRL_DOWN_MASK : 0;
int x = p.x + 3; // add some fudge
int y = p.y + 3; // add some fudge
int clickCount = 1;
boolean popupTrigger = false;
MouseEvent event = new MouseEvent(textField, MouseEvent.MOUSE_CLICKED, when, mods, x, y,
clickCount, popupTrigger);
runSwing(() -> textField.dispatchEvent(event));
}
private void toggleSearchModeViaKeyBinding() {
triggerKey(textField, InputEvent.CTRL_DOWN_MASK, KeyEvent.VK_DOWN, KeyEvent.CHAR_UNDEFINED);
}
private void toggleSearchModeViaKeyBinding_Backwards() {
triggerKey(textField, InputEvent.CTRL_DOWN_MASK, KeyEvent.VK_UP, KeyEvent.CHAR_UNDEFINED);
}
private void showMatchingList() { private void showMatchingList() {
runSwing(() -> textField.showMatchingList()); runSwing(() -> textField.showMatchingList());
} }
@@ -428,7 +428,10 @@ public class GProperties {
} }
} }
catch (Exception e) { 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; return null;
} }
@@ -88,6 +88,18 @@ public class ImageUtils {
return newImage; 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 * Crops the given image, keeping the given bounds
* *
@@ -474,4 +486,15 @@ public class ImageUtils {
destination[3] = rgbPixels[3]; destination[3] = rgbPixels[3];
return destination; 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) {
}
} }
@@ -0,0 +1,36 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ghidra.util;
/**
* An interface that can be added to the HelpService that signals the client has help that may
* change over time. The Help system will query this class to see if there is help for the
* registered object at the time help is requested. A client may register a static help location
* and an instance of this class with the Help system.
* <p>
* This can be used by a component to change the help location based on focus or mouse interaction.
* Typically a component will have one static help location. However, if that component has help
* for different areas within the component, then this interface allows that component to return
* any active help. This is useful for components that perform custom painting of regions, in
* which case that region has no object to use for adding help to the help system.
*/
public interface DynamicHelpLocation {
/**
* @return the current help location or null if there is currently no help for the client.
*/
public HelpLocation getActiveHelpLocation();
}
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -19,8 +19,7 @@ import java.awt.*;
import javax.swing.JButton; import javax.swing.JButton;
import ghidra.util.HelpLocation; import ghidra.util.*;
import ghidra.util.Msg;
import help.HelpDescriptor; import help.HelpDescriptor;
import help.HelpService; import help.HelpService;
@@ -64,6 +63,11 @@ public class DefaultHelpService implements HelpService {
// no-op // no-op
} }
@Override
public void registerDynamicHelp(Object helpObject, DynamicHelpLocation helpLocation) {
// no-op
}
@Override @Override
public HelpLocation getHelpLocation(Object object) { public HelpLocation getHelpLocation(Object object) {
return null; return null;
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -18,6 +18,7 @@ package help;
import java.awt.Component; import java.awt.Component;
import java.net.URL; import java.net.URL;
import ghidra.util.DynamicHelpLocation;
import ghidra.util.HelpLocation; import ghidra.util.HelpLocation;
/** /**
@@ -85,6 +86,14 @@ public interface HelpService {
*/ */
public void registerHelp(Object helpObject, HelpLocation helpLocation); 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, * Removes this object from the help system. This method is useful, for example,
* when a single Java {@link Component} will have different help locations * when a single Java {@link Component} will have different help locations
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 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 * 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; private PluginsConfiguration pluginClassManager;
DeafultPluginPackagingProvider(PluginsConfiguration pluginClassManager) { DefaultPluginPackagingProvider(PluginsConfiguration pluginClassManager) {
this.pluginClassManager = pluginClassManager; this.pluginClassManager = pluginClassManager;
} }
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -34,7 +34,7 @@ public class PluginConfigurationModel {
public PluginConfigurationModel(PluginTool tool) { public PluginConfigurationModel(PluginTool tool) {
this(new DefaultPluginInstaller(tool), this(new DefaultPluginInstaller(tool),
new DeafultPluginPackagingProvider(tool.getPluginsConfiguration())); new DefaultPluginPackagingProvider(tool.getPluginsConfiguration()));
} }
public PluginConfigurationModel(PluginInstaller pluginInstaller, public PluginConfigurationModel(PluginInstaller pluginInstaller,
@@ -31,7 +31,9 @@ import ghidra.framework.plugintool.PluginConfigurationModel;
import ghidra.framework.plugintool.PluginTool; import ghidra.framework.plugintool.PluginTool;
import ghidra.framework.plugintool.util.PluginPackage; import ghidra.framework.plugintool.util.PluginPackage;
import ghidra.util.HelpLocation; import ghidra.util.HelpLocation;
import ghidra.util.Msg;
import resources.Icons; import resources.Icons;
import utilities.util.reflection.ReflectionUtilities;
public class ManagePluginsDialog extends ReusableDialogComponentProvider { public class ManagePluginsDialog extends ReusableDialogComponentProvider {
@@ -145,6 +147,18 @@ public class ManagePluginsDialog extends ReusableDialogComponentProvider {
public boolean isEnabledForContext(ActionContext context) { public boolean isEnabledForContext(ActionContext context) {
return true; 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; icon = Icons.SAVE_AS_ICON;
saveAsAction saveAsAction
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@@ -15,9 +15,10 @@
*/ */
package help.screenshot; package help.screenshot;
import java.awt.Component; import java.awt.*;
import java.awt.Window; import java.util.ArrayList;
import java.util.*; import java.util.Arrays;
import java.util.List;
import javax.swing.*; import javax.swing.*;
@@ -25,6 +26,8 @@ import org.junit.Test;
import docking.ComponentProvider; import docking.ComponentProvider;
import docking.DialogComponentProvider; import docking.DialogComponentProvider;
import docking.util.image.Callout;
import docking.util.image.CalloutInfo;
import docking.widgets.DropDownSelectionTextField; import docking.widgets.DropDownSelectionTextField;
import docking.widgets.button.BrowseButton; import docking.widgets.button.BrowseButton;
import docking.widgets.tree.GTree; 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.editor.EnumEditorProvider;
import ghidra.app.plugin.core.datamgr.util.DataTypeChooserDialog; import ghidra.app.plugin.core.datamgr.util.DataTypeChooserDialog;
import ghidra.app.services.DataTypeManagerService; import ghidra.app.services.DataTypeManagerService;
import ghidra.app.util.datatype.DataTypeSelectionDialog;
import ghidra.app.util.datatype.DataTypeSelectionEditor;
import ghidra.program.model.data.*; import ghidra.program.model.data.*;
public class DataTypeEditorsScreenShots extends GhidraScreenShotGenerator { public class DataTypeEditorsScreenShots extends GhidraScreenShotGenerator {
public DataTypeEditorsScreenShots() {
}
@Test @Test
public void testDialog() { public void testDialog() {
@@ -48,6 +50,18 @@ public class DataTypeEditorsScreenShots extends GhidraScreenShotGenerator {
captureDialog(); captureDialog();
} }
@Test
public void testDialog_SearchMode() {
positionListingTop(0x40D3B8);
performAction("Choose Data Type", "DataPlugin", false);
captureDialog();
createSearchModeCallout();
cropExcessSpace();
}
@Test @Test
public void testDialog_Multiple_Match() throws Exception { public void testDialog_Multiple_Match() throws Exception {
@@ -142,6 +156,7 @@ public class DataTypeEditorsScreenShots extends GhidraScreenShotGenerator {
ComponentProvider structureEditor = getProvider(StructureEditorProvider.class); ComponentProvider structureEditor = getProvider(StructureEditorProvider.class);
// get structure table and select a row // get structure table and select a row
@SuppressWarnings("rawtypes")
CompositeEditorPanel editorPanel = CompositeEditorPanel editorPanel =
(CompositeEditorPanel) getInstanceField("editorPanel", structureEditor); (CompositeEditorPanel) getInstanceField("editorPanel", structureEditor);
JTable table = editorPanel.getTable(); JTable table = editorPanel.getTable();
@@ -178,6 +193,7 @@ public class DataTypeEditorsScreenShots extends GhidraScreenShotGenerator {
ComponentProvider structureEditor = getProvider(StructureEditorProvider.class); ComponentProvider structureEditor = getProvider(StructureEditorProvider.class);
// get structure table and select a row // get structure table and select a row
@SuppressWarnings("rawtypes")
CompositeEditorPanel editorPanel = CompositeEditorPanel editorPanel =
(CompositeEditorPanel) getInstanceField("editorPanel", structureEditor); (CompositeEditorPanel) getInstanceField("editorPanel", structureEditor);
JTable table = editorPanel.getTable(); JTable table = editorPanel.getTable();
@@ -203,6 +219,7 @@ public class DataTypeEditorsScreenShots extends GhidraScreenShotGenerator {
ComponentProvider structureEditor = getProvider(StructureEditorProvider.class); ComponentProvider structureEditor = getProvider(StructureEditorProvider.class);
// get structure table and select a row // get structure table and select a row
@SuppressWarnings("rawtypes")
CompositeEditorPanel editorPanel = CompositeEditorPanel editorPanel =
(CompositeEditorPanel) getInstanceField("editorPanel", structureEditor); (CompositeEditorPanel) getInstanceField("editorPanel", structureEditor);
JTable table = editorPanel.getTable(); JTable table = editorPanel.getTable();
@@ -262,6 +279,7 @@ public class DataTypeEditorsScreenShots extends GhidraScreenShotGenerator {
ComponentProvider structureEditor = getProvider(StructureEditorProvider.class); ComponentProvider structureEditor = getProvider(StructureEditorProvider.class);
// get structure table and select a row // get structure table and select a row
@SuppressWarnings("rawtypes")
CompositeEditorPanel editorPanel = CompositeEditorPanel editorPanel =
(CompositeEditorPanel) getInstanceField("editorPanel", structureEditor); (CompositeEditorPanel) getInstanceField("editorPanel", structureEditor);
JTable table = editorPanel.getTable(); JTable table = editorPanel.getTable();
@@ -404,4 +422,33 @@ public class DataTypeEditorsScreenShots extends GhidraScreenShotGenerator {
tool.execute(createDataCmd, program); tool.execute(createDataCmd, program);
waitForBusyTool(tool); waitForBusyTool(tool);
} }
private void cropExcessSpace() {
// keep the hover area and callout in the image (trial and error)
Rectangle area = new Rectangle();
area.x = 200;
area.y = 10;
area.width = 450;
area.height = 250;
crop(area);
}
private void createSearchModeCallout() {
DataTypeSelectionDialog dialog = waitForDialogComponent(DataTypeSelectionDialog.class);
DataTypeSelectionEditor editor = dialog.getEditor();
DropDownSelectionTextField<DataType> textField = editor.getDropDownTextField();
DropDownSelectionTextField<DataType>.SearchModeBounds searchModeBounds =
textField.getSearchModeBounds();
Rectangle hoverBounds = searchModeBounds.getHoverAreaBounds();
Window destinationComponent = SwingUtilities.windowForComponent(dialog.getComponent());
CalloutInfo calloutInfo =
new CalloutInfo(destinationComponent, textField, hoverBounds);
calloutInfo.setMagnification(2.75D); // make it a bit bigger than default
Callout callout = new Callout();
image = callout.createCalloutOnImage(image, calloutInfo);
}
} }
@@ -35,7 +35,7 @@ import docking.action.DockingAction;
import docking.menu.ActionState; import docking.menu.ActionState;
import docking.menu.MultiStateDockingAction; import docking.menu.MultiStateDockingAction;
import docking.util.image.Callout; import docking.util.image.Callout;
import docking.util.image.CalloutComponentInfo; import docking.util.image.CalloutInfo;
import docking.widgets.dialogs.MultiLineInputDialog; import docking.widgets.dialogs.MultiLineInputDialog;
import edu.uci.ics.jung.graph.Graph; import edu.uci.ics.jung.graph.Graph;
import edu.uci.ics.jung.visualization.VisualizationServer; import edu.uci.ics.jung.visualization.VisualizationServer;
@@ -61,14 +61,18 @@ import ghidra.util.exception.AssertException;
public class FunctionGraphPluginScreenShots extends AbstractFunctionGraphTest { 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 MyScreen screen;
private int width = 400; private int width = 400;
private int height = 400; private int height = 400;
public FunctionGraphPluginScreenShots() {
super();
}
@Override @Override
@Before @Before
public void setUp() throws Exception { public void setUp() throws Exception {
@@ -85,7 +89,7 @@ public class FunctionGraphPluginScreenShots extends AbstractFunctionGraphTest {
screen.program = program; screen.program = program;
setLayout(); setNestedLayout();
} }
@Override @Override
@@ -446,7 +450,7 @@ public class FunctionGraphPluginScreenShots extends AbstractFunctionGraphTest {
return dc.getHeader(); 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 // create image of parent with extra space for callout feature
Image parentImage = screen.captureComponent(parentComponent); Image parentImage = screen.captureComponent(parentComponent);
@@ -458,7 +462,7 @@ public class FunctionGraphPluginScreenShots extends AbstractFunctionGraphTest {
private void createGroupButtonCallout(FGVertex v) { private void createGroupButtonCallout(FGVertex v) {
JButton component = getToolbarButton(v, "Group Vertices"); JButton button = getToolbarButton(v, "Group Vertices");
FGProvider provider = screen.getProvider(FGProvider.class); FGProvider provider = screen.getProvider(FGProvider.class);
JComponent parent = provider.getComponent(); JComponent parent = provider.getComponent();
@@ -466,22 +470,23 @@ public class FunctionGraphPluginScreenShots extends AbstractFunctionGraphTest {
FGView view = controller.getView(); FGView view = controller.getView();
VisualizationViewer<FGVertex, FGEdge> viewer = view.getPrimaryGraphViewer(); VisualizationViewer<FGVertex, FGEdge> viewer = view.getPrimaryGraphViewer();
Rectangle bounds = component.getBounds(); Rectangle buttonBounds = button.getBounds();
Dimension size = bounds.getSize(); Point location = buttonBounds.getLocation();
Point location = bounds.getLocation();
JComponent vertexComponent = v.getComponent(); JComponent vertexComponent = v.getComponent();
Point newLocation = Point vertexRelativeLocation =
SwingUtilities.convertPoint(component.getParent(), location, vertexComponent); SwingUtilities.convertPoint(button.getParent(), location, vertexComponent);
Point relativePoint = GraphViewerUtils.translatePointFromVertexRelativeSpaceToViewSpace( Point buttonViewPoint = GraphViewerUtils.translatePointFromVertexRelativeSpaceToViewSpace(
viewer, v, newLocation); viewer, v, vertexRelativeLocation);
Rectangle buttonArea = new Rectangle(buttonViewPoint, buttonBounds.getSize());
Point screenLocation = new Point(relativePoint); // Use 'parent' for both source and destination. This has the effect of not moving any
SwingUtilities.convertPointToScreen(screenLocation, parent); // 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.
CalloutComponentInfo calloutInfo = new FGCalloutComponentInfo(parent, component, // It is done this way because the graph's vertices are painted as needed and are not
screenLocation, relativePoint, size, viewer, v); // connected to a real display hierarchy.
CalloutInfo calloutInfo = new CalloutInfo(parent, parent, buttonArea);
createCallout(parent, calloutInfo); createCallout(parent, calloutInfo);
} }
@@ -779,28 +784,6 @@ public class FunctionGraphPluginScreenShots extends AbstractFunctionGraphTest {
return reference.get(); return reference.get();
} }
private void setNestedLayout() {
Object actionManager = getInstanceField("actionManager", graphProvider);
@SuppressWarnings("unchecked")
final MultiStateDockingAction<Class<? extends FGLayoutProvider>> action =
(MultiStateDockingAction<Class<? extends FGLayoutProvider>>) getInstanceField(
"layoutAction", actionManager);
runSwing(() -> {
List<ActionState<Class<? extends FGLayoutProvider>>> states =
action.getAllActionStates();
for (ActionState<Class<? extends FGLayoutProvider>> state : states) {
Class<? extends FGLayoutProvider> layoutClass = state.getUserData();
if (layoutClass.getSimpleName().equals("DecompilerNestedLayoutProvider")) {
action.setCurrentActionState(state);
return;
}
}
throw new RuntimeException("Could not find layout!!");
});
}
private void createGroupButtonCallout_PlayArea(final FGVertex v, final String imageName) { private void createGroupButtonCallout_PlayArea(final FGVertex v, final String imageName) {
FGProvider provider = screen.getProvider(FGProvider.class); FGProvider provider = screen.getProvider(FGProvider.class);
@@ -832,32 +815,33 @@ public class FunctionGraphPluginScreenShots extends AbstractFunctionGraphTest {
dialog.setVisible(true); dialog.setVisible(true);
} }
@SuppressWarnings("rawtypes") private void setNestedLayout() {
private void setLayout() {
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
Object actionManager = getInstanceField("actionManager", graphProvider); Object actionManager = getInstanceField("actionManager", graphProvider);
final MultiStateDockingAction<?> action = @SuppressWarnings("unchecked")
(MultiStateDockingAction<?>) getInstanceField("layoutAction", actionManager); final MultiStateDockingAction<Class<? extends FGLayoutProvider>> action =
(MultiStateDockingAction<Class<? extends FGLayoutProvider>>) getInstanceField(
"layoutAction", actionManager);
runSwing(() -> {
List<ActionState<Class<? extends FGLayoutProvider>>> states =
action.getAllActionStates();
Object minCrossState = null; ActionState<Class<? extends FGLayoutProvider>> nestedCodeState = null;
List<?> states = action.getAllActionStates(); for (ActionState<Class<? extends FGLayoutProvider>> state : states) {
for (Object state : states) { if (state.getName().indexOf("Nested Code Layout") != -1) {
if (((ActionState) state).getName().indexOf("Nested Code Layout") != -1) { nestedCodeState = state;
minCrossState = state; break;
break; }
} }
}
assertNotNull("Could not find min cross layout!", minCrossState); assertNotNull("Could not find Nested Code Layout layout!", nestedCodeState);
//@formatter:off action.setCurrentActionState(nestedCodeState);
invokeInstanceMethod( "setCurrentActionState",
action,
new Class<?>[] { ActionState.class },
new Object[] { minCrossState });
//@formatter:on
runSwing(() -> action.actionPerformed(new DefaultActionContext())); // action.actionPerformed(new DefaultActionContext())
});
// wait for the threaded graph layout code // wait for the threaded graph layout code
FGController controller = getFunctionGraphController(); FGController controller = getFunctionGraphController();
@@ -868,6 +852,13 @@ public class FunctionGraphPluginScreenShots extends AbstractFunctionGraphTest {
long end = System.currentTimeMillis(); long end = System.currentTimeMillis();
Msg.debug(this, "relayout time: " + ((end - start) / 1000.0) + "s"); 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; return helpTopicDir;
} }
} }
private class FGCalloutComponentInfo extends CalloutComponentInfo {
private VisualizationViewer<FGVertex, FGEdge> viewer;
private FGVertex vertex;
FGCalloutComponentInfo(Component destinationComponent, Component component,
Point locationOnScreen, Point relativeLocation, Dimension size,
VisualizationViewer<FGVertex, FGEdge> viewer, FGVertex vertex) {
super(destinationComponent, component, locationOnScreen, relativeLocation, size);
this.viewer = viewer;
this.vertex = vertex;
}
@Override
public Point convertPointToParent(Point location) {
// TODO: this won't work for now if the graph is scaled. This is because there is
// point information that is calculated by the client of this class that does
// not take into account the scaling of the graph. This is a known issue--
// don't use this class when the graph is scaled.
return location;
}
}
} }
@@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * http://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * 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 org.junit.Test;
import docking.DockableComponent;
import docking.menu.MultiStateDockingAction; import docking.menu.MultiStateDockingAction;
import docking.util.AnimationUtils; import docking.util.AnimationUtils;
import docking.util.image.Callout; import docking.util.image.Callout;
import docking.util.image.CalloutComponentInfo; import docking.util.image.CalloutInfo;
import docking.widgets.EmptyBorderButton; import docking.widgets.EmptyBorderButton;
import docking.widgets.filter.*; import docking.widgets.filter.*;
import docking.widgets.table.columnfilter.ColumnBasedTableFilter; 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 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 location to a value that is relative to the image (we created the image above by
capturing the provider using it's DockableComponent). 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); Component dc = getDockableComponent(provider);
CalloutInfo calloutInfo = new CalloutInfo(dc, label);
CalloutComponentInfo calloutInfo = new CalloutComponentInfo(dc, label);
calloutInfo.setMagnification(2.75D); // make it a bit bigger than default calloutInfo.setMagnification(2.75D); // make it a bit bigger than default
Callout callout = new Callout(); Callout callout = new Callout();
image = callout.createCalloutOnImage(image, calloutInfo); image = callout.createCalloutOnImage(image, calloutInfo);
@@ -104,8 +107,8 @@ public class TreesScreenShots extends GhidraScreenShotGenerator {
Rectangle area = new Rectangle(); Rectangle area = new Rectangle();
int height = 275; int height = 275;
area.x = 0; area.x = 0;
area.y = 80; area.y = 60;
area.width = 560; area.width = 580;
area.height = height - area.y; area.height = height - area.y;
crop(area); crop(area);
} }