mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-10-03 09:49:23 +02:00
Merge remote-tracking branch
'origin/GP-5720-dragonmacher-drop-down-field-contains-mode--SQUASHED' (Closes #4725, Closes #8203)
This commit is contained in:
commit
a48c081e61
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
|
@ -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);
|
||||||
|
|
|
@ -17,25 +17,35 @@ package docking.widgets;
|
||||||
|
|
||||||
import java.awt.*;
|
import java.awt.*;
|
||||||
import java.awt.event.*;
|
import java.awt.event.*;
|
||||||
|
import java.awt.font.FontRenderContext;
|
||||||
|
import java.awt.font.GlyphVector;
|
||||||
|
import java.awt.geom.Rectangle2D;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import javax.swing.*;
|
import javax.swing.*;
|
||||||
import javax.swing.border.BevelBorder;
|
import javax.swing.border.BevelBorder;
|
||||||
import javax.swing.event.*;
|
import javax.swing.event.*;
|
||||||
|
import javax.swing.text.Caret;
|
||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
|
|
||||||
|
import docking.DockingWindowManager;
|
||||||
|
import docking.widgets.DropDownTextFieldDataModel.SearchMode;
|
||||||
import docking.widgets.label.GDHtmlLabel;
|
import docking.widgets.label.GDHtmlLabel;
|
||||||
import docking.widgets.list.GList;
|
import docking.widgets.list.GList;
|
||||||
import generic.theme.GColor;
|
import generic.theme.GColor;
|
||||||
|
import generic.theme.GThemeDefaults.Colors;
|
||||||
|
import generic.theme.GThemeDefaults.Colors.Messages;
|
||||||
import generic.theme.GThemeDefaults.Colors.Tooltips;
|
import generic.theme.GThemeDefaults.Colors.Tooltips;
|
||||||
import generic.util.WindowUtilities;
|
import generic.util.WindowUtilities;
|
||||||
import ghidra.util.StringUtilities;
|
import ghidra.framework.options.PreferenceState;
|
||||||
import ghidra.util.SystemUtilities;
|
import ghidra.util.*;
|
||||||
import ghidra.util.datastruct.WeakDataStructureFactory;
|
import ghidra.util.datastruct.WeakDataStructureFactory;
|
||||||
import ghidra.util.datastruct.WeakSet;
|
import ghidra.util.datastruct.WeakSet;
|
||||||
import ghidra.util.task.SwingUpdateManager;
|
import ghidra.util.task.SwingUpdateManager;
|
||||||
|
import help.Help;
|
||||||
|
import help.HelpService;
|
||||||
import util.CollectionUtils;
|
import util.CollectionUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -60,6 +70,8 @@ import util.CollectionUtils;
|
||||||
*/
|
*/
|
||||||
public class DropDownTextField<T> extends JTextField implements GComponent {
|
public class DropDownTextField<T> extends JTextField implements GComponent {
|
||||||
|
|
||||||
|
private static final Cursor CURSOR_HAND = Cursor.getPredefinedCursor(Cursor.HAND_CURSOR);
|
||||||
|
private static final Cursor CURSOR_DEFAULT = Cursor.getDefaultCursor();
|
||||||
private static final int DEFAULT_MAX_UPDATE_DELAY = 2000;
|
private static final int DEFAULT_MAX_UPDATE_DELAY = 2000;
|
||||||
private static final int MIN_HEIGHT = 300;
|
private static final int MIN_HEIGHT = 300;
|
||||||
private static final int MIN_WIDTH = 200;
|
private static final int MIN_WIDTH = 200;
|
||||||
|
@ -90,7 +102,7 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
|
||||||
protected boolean internallyDrivenUpdate;
|
protected boolean internallyDrivenUpdate;
|
||||||
private boolean consumeEnterKeyPress = true; // consume Enter presses by default
|
private boolean consumeEnterKeyPress = true; // consume Enter presses by default
|
||||||
private boolean ignoreEnterKeyPress = false; // do not ignore enter by default
|
private boolean ignoreEnterKeyPress = false; // do not ignore enter by default
|
||||||
private boolean ignoreCaretChanges;
|
private boolean textFieldNotFocused;
|
||||||
private boolean showMachingListOnEmptyText;
|
private boolean showMachingListOnEmptyText;
|
||||||
|
|
||||||
// We use an update manager to buffer requests to update the matches. This allows us to be
|
// We use an update manager to buffer requests to update the matches. This allows us to be
|
||||||
|
@ -106,6 +118,16 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
|
||||||
*/
|
*/
|
||||||
private String currentMatchingText;
|
private String currentMatchingText;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search mode support. Clients specify search modes that allow the user to change how results
|
||||||
|
* are matched. For backward compatibility, this will be empty for clients that have not
|
||||||
|
* specified search modes.
|
||||||
|
*/
|
||||||
|
private List<SearchMode> searchModes = new ArrayList<>();
|
||||||
|
private boolean searchModeIsHovered;
|
||||||
|
private SearchMode searchMode = SearchMode.UNKNOWN;
|
||||||
|
private SearchModeBounds searchModeBounds;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Constructor.
|
* Constructor.
|
||||||
* <p>
|
* <p>
|
||||||
|
@ -132,7 +154,36 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
|
||||||
init(updateMinDelay);
|
init(updateMinDelay);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateUI() {
|
||||||
|
|
||||||
|
// reset the hint bounds; this value is based on the current font
|
||||||
|
searchModeBounds = null;
|
||||||
|
|
||||||
|
super.updateUI();
|
||||||
|
}
|
||||||
|
|
||||||
private void init(int updateMinDelay) {
|
private void init(int updateMinDelay) {
|
||||||
|
|
||||||
|
List<SearchMode> modes = dataModel.getSupportedSearchModes();
|
||||||
|
for (SearchMode mode : modes) {
|
||||||
|
if (mode != SearchMode.UNKNOWN && !searchModes.contains(mode)) {
|
||||||
|
searchModes.add(mode);
|
||||||
|
|
||||||
|
// pick the first mode to use
|
||||||
|
if (searchMode == SearchMode.UNKNOWN) {
|
||||||
|
searchMode = mode;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
installSearchModeDisplay();
|
||||||
|
|
||||||
|
// add a one-time listener to this field to restore any saved state, like the search mode
|
||||||
|
DockingWindowManager.registerComponentLoadedListener(this, (dwm, provider) -> {
|
||||||
|
loadPreferenceState();
|
||||||
|
});
|
||||||
|
|
||||||
updateManager = new SwingUpdateManager(updateMinDelay, DEFAULT_MAX_UPDATE_DELAY,
|
updateManager = new SwingUpdateManager(updateMinDelay, DEFAULT_MAX_UPDATE_DELAY,
|
||||||
"Drop Down Selection Text Field Update Manager", () -> {
|
"Drop Down Selection Text Field Update Manager", () -> {
|
||||||
if (pendingTextUpdate == null) {
|
if (pendingTextUpdate == null) {
|
||||||
|
@ -151,6 +202,121 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
|
||||||
initDataList();
|
initDataList();
|
||||||
|
|
||||||
getAccessibleContext().setAccessibleName("Data Type Editor");
|
getAccessibleContext().setAccessibleName("Data Type Editor");
|
||||||
|
|
||||||
|
HelpService help = Help.getHelpService();
|
||||||
|
help.registerDynamicHelp(this, new SearchModeHelpLocation());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void installSearchModeDisplay() {
|
||||||
|
|
||||||
|
if (!hasMultipleSearchModes()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
addComponentListener(new ComponentAdapter() {
|
||||||
|
@Override
|
||||||
|
public void componentResized(ComponentEvent e) {
|
||||||
|
// when resized, update the location of the search mode hint when we get repainted
|
||||||
|
searchModeBounds = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
SearchModeMouseListener mouseListener = new SearchModeMouseListener();
|
||||||
|
addMouseMotionListener(mouseListener);
|
||||||
|
addMouseListener(mouseListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean hasMultipleSearchModes() {
|
||||||
|
return searchModes.size() > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isOverSearchMode(MouseEvent e) {
|
||||||
|
if (searchModeBounds == null) {
|
||||||
|
return false; // have not yet been painted
|
||||||
|
}
|
||||||
|
|
||||||
|
Point p = e.getPoint();
|
||||||
|
return searchModeBounds.isHovered(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SearchMode getSearchMode() {
|
||||||
|
return searchMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSearchMode(SearchMode newMode) {
|
||||||
|
|
||||||
|
if (!searchModes.contains(newMode)) {
|
||||||
|
throw new IllegalArgumentException(
|
||||||
|
"Search mode is not supported by this texts field: " + newMode);
|
||||||
|
}
|
||||||
|
doSetSearchMode(newMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void doSetSearchMode(SearchMode newMode) {
|
||||||
|
searchMode = newMode;
|
||||||
|
searchModeBounds = null;
|
||||||
|
repaint();
|
||||||
|
|
||||||
|
savePreferenceState();
|
||||||
|
|
||||||
|
maybeUpdateDisplayContents(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void toggleSearchMode(boolean forward) {
|
||||||
|
|
||||||
|
if (!hasMultipleSearchModes()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int index = searchModes.indexOf(searchMode);
|
||||||
|
int next = forward ? index + 1 : index - 1;
|
||||||
|
if (forward) {
|
||||||
|
if (next == searchModes.size()) {
|
||||||
|
next = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (next == -1) {
|
||||||
|
next = searchModes.size() - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchMode newMode = searchModes.get(next);
|
||||||
|
doSetSearchMode(newMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void savePreferenceState() {
|
||||||
|
|
||||||
|
String preferenceKey = dataModel.getClass().getSimpleName();
|
||||||
|
PreferenceState state = new PreferenceState();
|
||||||
|
state.putEnum("searchMode", searchMode);
|
||||||
|
|
||||||
|
// We are in the UI at this point, so we have a valid window manager. (The window manager
|
||||||
|
// may be null in testing.)
|
||||||
|
DockingWindowManager dwm = DockingWindowManager.getInstance(this);
|
||||||
|
if (dwm != null) {
|
||||||
|
dwm.putPreferenceState(preferenceKey, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadPreferenceState() {
|
||||||
|
String preferenceKey = dataModel.getClass().getSimpleName();
|
||||||
|
|
||||||
|
// We are in the UI at this point, so we have a valid window manager. (The window manager
|
||||||
|
// may be null in testing.)
|
||||||
|
DockingWindowManager dwm = DockingWindowManager.getInstance(this);
|
||||||
|
if (dwm == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
PreferenceState state = dwm.getPreferenceState(preferenceKey);
|
||||||
|
if (state == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
searchMode = state.getEnum("searchMode", searchMode);
|
||||||
|
searchModeBounds = null;
|
||||||
|
repaint();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected ListSelectionModel createListSelectionModel() {
|
protected ListSelectionModel createListSelectionModel() {
|
||||||
|
@ -300,11 +466,44 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
|
||||||
updateManager.updateLater();
|
updateManager.updateLater();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void maybeUpdateDisplayContents(String userText) {
|
private void maybeUpdateDisplayContents(boolean force) {
|
||||||
if (SystemUtilities.isEqual(userText, pendingTextUpdate)) {
|
if (textFieldNotFocused) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
updateDisplayContents(userText);
|
|
||||||
|
String text = getText();
|
||||||
|
if (StringUtils.isBlank(text)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// caret position only matters with 'starts with', as the user can arrow through the text
|
||||||
|
// to change which text the 'starts with' matches
|
||||||
|
if (!isStartsWithSearch()) {
|
||||||
|
if (force || isDifferentText(text)) {
|
||||||
|
updateDisplayContents(text);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Caret caret = getCaret();
|
||||||
|
int dot = caret.getDot();
|
||||||
|
String textToCaret = text.substring(0, dot);
|
||||||
|
if (force || isDifferentText(textToCaret)) {
|
||||||
|
updateDisplayContents(textToCaret);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isDifferentText(String newText) {
|
||||||
|
return !CollectionUtils.isOneOf(newText, currentMatchingText, pendingTextUpdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isStartsWithSearch() {
|
||||||
|
if (hasMultipleSearchModes()) {
|
||||||
|
return searchMode == SearchMode.STARTS_WITH;
|
||||||
|
}
|
||||||
|
|
||||||
|
return searchMode == SearchMode.STARTS_WITH ||
|
||||||
|
searchMode == SearchMode.UNKNOWN; // backward compatibility
|
||||||
}
|
}
|
||||||
|
|
||||||
private void doUpdateDisplayContents(String userText) {
|
private void doUpdateDisplayContents(String userText) {
|
||||||
|
@ -367,7 +566,13 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
|
||||||
Cursor previousCursor = getCursor();
|
Cursor previousCursor = getCursor();
|
||||||
try {
|
try {
|
||||||
setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
|
setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
|
||||||
return dataModel.getMatchingData(searchText);
|
|
||||||
|
if (searchMode == SearchMode.UNKNOWN) {
|
||||||
|
// backward compatible
|
||||||
|
return dataModel.getMatchingData(searchText);
|
||||||
|
}
|
||||||
|
return dataModel.getMatchingData(searchText, searchMode);
|
||||||
|
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
setCursor(previousCursor);
|
setCursor(previousCursor);
|
||||||
|
@ -383,12 +588,13 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Shows the matching list. This can be used to show all data when the user has not typed any
|
* Shows the matching list. This can be used to show all data when the user has not typed any
|
||||||
* text.
|
* text. For data models that have large data sets, this call may not show the matching list.
|
||||||
|
* This behavior is determine by the current data model.
|
||||||
*/
|
*/
|
||||||
public void showMatchingList() {
|
public void showMatchingList() {
|
||||||
|
|
||||||
//
|
//
|
||||||
// We temporarily enable this list to show for empty text, even if the text is not empty.
|
// We temporarily enable this list to show for empty text, even if the text is not empty.
|
||||||
// This handles the default setting, which has this feature off. We can refactor this class
|
// This handles the default setting, which has this feature off. We can refactor this class
|
||||||
// to allow us to make a direct call instead of using this temporary setting. This seems
|
// to allow us to make a direct call instead of using this temporary setting. This seems
|
||||||
// simple enough for now.
|
// simple enough for now.
|
||||||
|
@ -702,6 +908,66 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
|
||||||
windowVisibilityListener = Objects.requireNonNull(l);
|
windowVisibilityListener = Objects.requireNonNull(l);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void paintComponent(Graphics g) {
|
||||||
|
super.paintComponent(g);
|
||||||
|
|
||||||
|
if (searchMode == SearchMode.UNKNOWN) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String modeHint = searchMode.getHint();
|
||||||
|
searchModeBounds = calculateSearchModeBounds(modeHint, g);
|
||||||
|
|
||||||
|
Color textColor = searchModeIsHovered ? Colors.FOREGROUND : Messages.HINT;
|
||||||
|
|
||||||
|
Graphics2D g2 = (Graphics2D) g;
|
||||||
|
g2.setColor(textColor);
|
||||||
|
g2.setFont(g2.getFont().deriveFont(Font.ITALIC));
|
||||||
|
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||||
|
|
||||||
|
Dimension size = getSize();
|
||||||
|
Insets insets = getInsets();
|
||||||
|
int bottomPad = 3;
|
||||||
|
int x = searchModeBounds.getTextStartX();
|
||||||
|
int y = size.height - (insets.bottom + bottomPad); // strings paint bottom-up
|
||||||
|
|
||||||
|
g2.drawString(modeHint, x, y);
|
||||||
|
|
||||||
|
// debug
|
||||||
|
// g.setColor(Color.ORANGE);
|
||||||
|
// g2.draw(searchModeBounds.hoverAreaBounds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private SearchModeBounds calculateSearchModeBounds(String text, Graphics g) {
|
||||||
|
if (searchModeBounds != null) {
|
||||||
|
return searchModeBounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
Graphics2D g2d = (Graphics2D) g;
|
||||||
|
Font f = g.getFont();
|
||||||
|
FontRenderContext frc = g2d.getFontRenderContext();
|
||||||
|
char[] chars = text.toCharArray();
|
||||||
|
int n = text.length();
|
||||||
|
GlyphVector gv = f.layoutGlyphVector(frc, chars, 0, n, Font.LAYOUT_LEFT_TO_RIGHT);
|
||||||
|
Rectangle2D bounds2d = gv.getVisualBounds();
|
||||||
|
|
||||||
|
searchModeBounds = new SearchModeBounds(bounds2d.getBounds());
|
||||||
|
return searchModeBounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the search mode bounds. This is the area of the text field that shows the current
|
||||||
|
* search mode. This area can be hovered and clicked by the user. If there are not multiple
|
||||||
|
* search modes available, then this area is not painted and the bounds will be null. This
|
||||||
|
* value will get updated as this text field is resized.
|
||||||
|
*
|
||||||
|
* @return the search mode bounds
|
||||||
|
*/
|
||||||
|
public SearchModeBounds getSearchModeBounds() {
|
||||||
|
return searchModeBounds;
|
||||||
|
}
|
||||||
|
|
||||||
//=================================================================================================
|
//=================================================================================================
|
||||||
// Inner Classes
|
// Inner Classes
|
||||||
//=================================================================================================
|
//=================================================================================================
|
||||||
|
@ -738,13 +1004,13 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
ignoreCaretChanges = true;
|
textFieldNotFocused = true;
|
||||||
hideMatchingWindow();
|
hideMatchingWindow();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void focusGained(FocusEvent e) {
|
public void focusGained(FocusEvent e) {
|
||||||
ignoreCaretChanges = false;
|
textFieldNotFocused = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -777,21 +1043,7 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
|
||||||
private class UpdateCaretListener implements CaretListener {
|
private class UpdateCaretListener implements CaretListener {
|
||||||
@Override
|
@Override
|
||||||
public void caretUpdate(CaretEvent event) {
|
public void caretUpdate(CaretEvent event) {
|
||||||
if (ignoreCaretChanges) {
|
maybeUpdateDisplayContents(false);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String text = getText();
|
|
||||||
if (text == null || text.isEmpty()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
String textToCaret = text.substring(0, event.getDot());
|
|
||||||
if (textToCaret.equals(currentMatchingText)) {
|
|
||||||
return; // nothing to do
|
|
||||||
}
|
|
||||||
|
|
||||||
maybeUpdateDisplayContents(textToCaret);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -910,21 +1162,33 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
|
||||||
|
|
||||||
private void handleArrowKey(KeyEvent event) {
|
private void handleArrowKey(KeyEvent event) {
|
||||||
|
|
||||||
|
if (getMatchingWindow().isShowing()) {
|
||||||
|
handleArrowKeyForMatchingWindow(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Contrl-Up/Down is for toggling the search mode
|
||||||
|
if (event.isControlDown()) {
|
||||||
|
int keyCode = event.getKeyCode();
|
||||||
|
boolean forward = keyCode == KeyEvent.VK_DOWN || keyCode == KeyEvent.VK_KP_DOWN;
|
||||||
|
toggleSearchMode(forward);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDisplayContents(getText());
|
||||||
|
event.consume();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleArrowKeyForMatchingWindow(KeyEvent event) {
|
||||||
int keyCode = event.getKeyCode();
|
int keyCode = event.getKeyCode();
|
||||||
if (!getMatchingWindow().isShowing()) {
|
if (keyCode == KeyEvent.VK_UP || keyCode == KeyEvent.VK_KP_UP) {
|
||||||
updateDisplayContents(getText());
|
decrementListSelection();
|
||||||
event.consume();
|
|
||||||
}
|
}
|
||||||
else { // update the window if it is showing
|
else {
|
||||||
if (keyCode == KeyEvent.VK_UP || keyCode == KeyEvent.VK_KP_UP) {
|
incrementListSelection();
|
||||||
decrementListSelection();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
incrementListSelection();
|
|
||||||
}
|
|
||||||
event.consume();
|
|
||||||
setTextFromSelectedListItemAndKeepMatchingWindowOpen();
|
|
||||||
}
|
}
|
||||||
|
event.consume();
|
||||||
|
setTextFromSelectedListItemAndKeepMatchingWindowOpen();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void incrementListSelection() {
|
private void incrementListSelection() {
|
||||||
|
@ -1038,6 +1302,108 @@ public class DropDownTextField<T> extends JTextField implements GComponent {
|
||||||
public void setLeadSelectionIndex(int leadIndex) {
|
public void setLeadSelectionIndex(int leadIndex) {
|
||||||
// stub
|
// stub
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class SearchModeMouseListener extends MouseAdapter {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseClicked(MouseEvent e) {
|
||||||
|
if (e.getClickCount() != 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isOverSearchMode(e)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean forward = !e.isControlDown();
|
||||||
|
toggleSearchMode(forward);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateSearchModeHover(MouseEvent e) {
|
||||||
|
searchModeIsHovered = isOverSearchMode(e);
|
||||||
|
String tip =
|
||||||
|
searchModeIsHovered ? "Search Mode: " + searchMode.getDisplayName() : null;
|
||||||
|
setToolTipText(tip);
|
||||||
|
setCursor(searchModeIsHovered ? CURSOR_HAND : CURSOR_DEFAULT);
|
||||||
|
repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseMoved(MouseEvent e) {
|
||||||
|
updateSearchModeHover(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseEntered(MouseEvent e) {
|
||||||
|
updateSearchModeHover(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void mouseExited(MouseEvent e) {
|
||||||
|
updateSearchModeHover(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class SearchModeHelpLocation implements DynamicHelpLocation {
|
||||||
|
|
||||||
|
// Note the help for this generic field currently lives in the help for the Data Type
|
||||||
|
// chooser, which is a bit odd, but convenient. To fix this, we would need a separate help
|
||||||
|
// page for the generic text field.
|
||||||
|
private HelpLocation helpLocation = new HelpLocation("DataTypeEditors", "SearchMode");
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public HelpLocation getActiveHelpLocation() {
|
||||||
|
if (searchModeIsHovered) {
|
||||||
|
return helpLocation;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents the bounds of the search mode area in this text field. This also tracks the text
|
||||||
|
* position within the search mode bounds.
|
||||||
|
*/
|
||||||
|
public class SearchModeBounds {
|
||||||
|
private Rectangle textBounds;
|
||||||
|
private Rectangle hoverAreaBounds;
|
||||||
|
|
||||||
|
SearchModeBounds(Rectangle textBounds) {
|
||||||
|
this.textBounds = textBounds;
|
||||||
|
|
||||||
|
Dimension size = getSize();
|
||||||
|
Insets insets = getInsets();
|
||||||
|
hoverAreaBounds = new Rectangle(textBounds);
|
||||||
|
hoverAreaBounds.width += 10; // add some padding
|
||||||
|
|
||||||
|
// same height as this field
|
||||||
|
hoverAreaBounds.height = getHeight() - (insets.top + insets.bottom);
|
||||||
|
|
||||||
|
// move away from the end of this field
|
||||||
|
hoverAreaBounds.x = size.width - insets.right - hoverAreaBounds.width;
|
||||||
|
hoverAreaBounds.y = insets.top;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Rectangle getHoverAreaBounds() {
|
||||||
|
return hoverAreaBounds;
|
||||||
|
}
|
||||||
|
|
||||||
|
boolean isHovered(Point p) {
|
||||||
|
return hoverAreaBounds.contains(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
Point getLocation() {
|
||||||
|
return hoverAreaBounds.getLocation();
|
||||||
|
}
|
||||||
|
|
||||||
|
int getTextWidth() {
|
||||||
|
return textBounds.width;
|
||||||
|
}
|
||||||
|
|
||||||
|
int getTextStartX() {
|
||||||
|
return (int) hoverAreaBounds.getCenterX() - (getTextWidth() / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue