mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2024-11-21 11:31:43 +00:00
GP-5049 - Refactor of hyperlink component to support accessibility
This commit is contained in:
parent
aaa19420e9
commit
8a6145e2df
@ -1,285 +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 ghidra.features.bsim.gui.search.results;
|
||||
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Color;
|
||||
import java.util.*;
|
||||
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.event.HyperlinkEvent;
|
||||
|
||||
import docking.widgets.HyperlinkComponent;
|
||||
import docking.widgets.table.DiscoverableTableUtils;
|
||||
import docking.widgets.table.TableColumnDescriptor;
|
||||
import generic.theme.GThemeDefaults.Colors.Palette;
|
||||
import ghidra.app.plugin.core.table.TableComponentProvider;
|
||||
import ghidra.app.util.query.TableService;
|
||||
import ghidra.docking.settings.Settings;
|
||||
import ghidra.features.bsim.query.facade.SFQueryResult;
|
||||
import ghidra.features.bsim.query.protocol.SimilarityResult;
|
||||
import ghidra.framework.plugintool.ServiceProvider;
|
||||
import ghidra.program.database.symbol.FunctionSymbol;
|
||||
import ghidra.program.model.address.Address;
|
||||
import ghidra.program.model.listing.Function;
|
||||
import ghidra.program.model.listing.Program;
|
||||
import ghidra.program.util.ProgramLocation;
|
||||
import ghidra.util.Msg;
|
||||
import ghidra.util.datastruct.Accumulator;
|
||||
import ghidra.util.exception.CancelledException;
|
||||
import ghidra.util.table.AddressBasedTableModel;
|
||||
import ghidra.util.table.field.*;
|
||||
import ghidra.util.task.TaskMonitor;
|
||||
|
||||
/**
|
||||
* Panel that displays the list of functions to be included in a BSim query.
|
||||
*
|
||||
*/
|
||||
class DisplayFunctionsPanel extends JPanel {
|
||||
|
||||
private static final String SHOW_TABLE_HREF_NAME = "ShowTable";
|
||||
|
||||
private static final Color MARKER_COLOR = Palette.getColor("lightskyblue");
|
||||
|
||||
private HyperlinkComponent functionsHTMLComponent;
|
||||
|
||||
private TableComponentProvider<?> tableProvider;
|
||||
private final ServiceProvider serviceProvider;
|
||||
private Set<FunctionSymbol> selectedFunctions;
|
||||
|
||||
// Maps input functions to the number of matches associated with it. This is
|
||||
// here to provide quick access for the MatchCountTableColumn.
|
||||
private Map<String, Integer> functionMatchMap = new HashMap<>();
|
||||
private String description;
|
||||
|
||||
DisplayFunctionsPanel(ServiceProvider serviceProvider, String desc) {
|
||||
super(new BorderLayout());
|
||||
this.serviceProvider = serviceProvider;
|
||||
this.description = desc;
|
||||
|
||||
functionsHTMLComponent = new HyperlinkComponent(getNoFunctionsSelectedMessage());
|
||||
functionsHTMLComponent.addHyperlinkListener(SHOW_TABLE_HREF_NAME, e -> {
|
||||
if (e.getEventType() != HyperlinkEvent.EventType.ACTIVATED) {
|
||||
// not a mouse click
|
||||
return;
|
||||
}
|
||||
showAllSelectedFunctionsTable();
|
||||
});
|
||||
|
||||
add(functionsHTMLComponent, BorderLayout.CENTER);
|
||||
}
|
||||
|
||||
/**
|
||||
* Takes a new set of query results and parses the function counts.
|
||||
*
|
||||
* @param queryResult the object to hold the load results
|
||||
*/
|
||||
public void loadQueryResults(SFQueryResult queryResult) {
|
||||
parseFunctionMatchCounts(queryResult);
|
||||
}
|
||||
|
||||
private String getNoFunctionsSelectedMessage() {
|
||||
StringBuilder buffy = new StringBuilder();
|
||||
buffy.append(
|
||||
"<html><font color=\"" + Palette.GRAY + "\"><i>No functions selected</i></font>");
|
||||
return buffy.toString();
|
||||
}
|
||||
|
||||
void close() {
|
||||
if (tableProvider != null) {
|
||||
tableProvider.removeFromTool();
|
||||
tableProvider = null;
|
||||
}
|
||||
|
||||
selectedFunctions = null;
|
||||
}
|
||||
|
||||
void setSelectedFunctions(Set<FunctionSymbol> functions) {
|
||||
|
||||
this.selectedFunctions = functions;
|
||||
if (tableProvider != null && tableProvider.isInTool()) {
|
||||
tableProvider.removeFromTool();
|
||||
tableProvider = null;
|
||||
}
|
||||
|
||||
if (functions.isEmpty()) {
|
||||
functionsHTMLComponent.setText(getNoFunctionsSelectedMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
String text = createTextForSelectedFunctions(functions);
|
||||
functionsHTMLComponent.setText(text);
|
||||
}
|
||||
|
||||
private String createTextForSelectedFunctions(Set<FunctionSymbol> functions) {
|
||||
|
||||
if (functions.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder buffy = new StringBuilder();
|
||||
int count = functions.size();
|
||||
Function firstFunc = (Function) functions.iterator().next().getObject();
|
||||
String programName = firstFunc.getProgram().getDomainFile().getPathname();
|
||||
buffy.append(description).append(" ");
|
||||
buffy.append(firstFunc.getName());
|
||||
if (count > 1) {
|
||||
buffy.append(" and ");
|
||||
buffy.append(count - 1);
|
||||
buffy.append(" other function");
|
||||
}
|
||||
if (count > 2) {
|
||||
buffy.append("s");
|
||||
}
|
||||
buffy.append(" from ");
|
||||
buffy.append(programName);
|
||||
buffy.append(" <a href=\"").append(SHOW_TABLE_HREF_NAME).append("\">"); // open anchor
|
||||
buffy.append("<font color=\"" + Palette.BLUE + "\">");
|
||||
buffy.append(" (show table) ");
|
||||
buffy.append("</font>");
|
||||
buffy.append("</a>"); // close anchor
|
||||
return buffy.toString();
|
||||
}
|
||||
|
||||
private void showAllSelectedFunctionsTable() {
|
||||
if (tableProvider != null) {
|
||||
if (tableProvider.isInTool()) {
|
||||
tableProvider.setVisible(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// it has been closed--cleanup
|
||||
tableProvider = null;
|
||||
}
|
||||
|
||||
TableService tableService = serviceProvider.getService(TableService.class);
|
||||
if (tableService == null) {
|
||||
Msg.showWarn(getClass(), this, "No Table Service Found",
|
||||
"Unable to locate the Table Service. Make sure the plugin is installed.");
|
||||
return;
|
||||
}
|
||||
|
||||
FunctionSymbol arbitraryFunction = selectedFunctions.iterator().next();
|
||||
Program program = arbitraryFunction.getProgram();
|
||||
SelectedFunctionsModel model =
|
||||
new SelectedFunctionsModel(program, serviceProvider, selectedFunctions);
|
||||
tableProvider = tableService.showTableWithMarkers("Selected Query Functions",
|
||||
"QueryDialogTable", model, MARKER_COLOR, null /*icon*/, "Selected Query Functions",
|
||||
null /*navigatable - use default*/);
|
||||
}
|
||||
|
||||
//==================================================================================================
|
||||
// Inner Classes
|
||||
//==================================================================================================
|
||||
|
||||
private class SelectedFunctionsModel extends AddressBasedTableModel<Function> {
|
||||
private final List<FunctionSymbol> functions;
|
||||
|
||||
SelectedFunctionsModel(Program program, ServiceProvider serviceProvider,
|
||||
Set<FunctionSymbol> functions) {
|
||||
super("Selected Query Functions", serviceProvider, program, null);
|
||||
this.functions = new ArrayList<>(functions);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected TableColumnDescriptor<Function> createTableColumnDescriptor() {
|
||||
TableColumnDescriptor<Function> descriptor = new TableColumnDescriptor<>();
|
||||
|
||||
descriptor.addVisibleColumn(
|
||||
DiscoverableTableUtils.adaptColumForModel(this, new LabelTableColumn()));
|
||||
descriptor.addVisibleColumn(
|
||||
DiscoverableTableUtils.adaptColumForModel(this, new AddressTableColumn()), 1, true);
|
||||
descriptor.addVisibleColumn(DiscoverableTableUtils.adaptColumForModel(this,
|
||||
new FunctionSignatureTableColumn()));
|
||||
descriptor.addVisibleColumn(new MatchCountTableColumn());
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Address getAddress(int row) {
|
||||
Function function = getRowObject(row);
|
||||
return function.getEntryPoint();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doLoad(Accumulator<Function> accumulator, TaskMonitor monitor)
|
||||
throws CancelledException {
|
||||
for (FunctionSymbol sym : functions) {
|
||||
Object obj = sym.getObject();
|
||||
if (obj != null) {
|
||||
accumulator.add((Function) obj);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProgramLocation getProgramLocation(int row, int column) {
|
||||
// TODO: I cannot reconcile how to show the user a table of functions and let them
|
||||
// navigate in a way that is understandable (if they navigate, that changes the selected
|
||||
// functions, which would then clear the table)
|
||||
//
|
||||
// Sad Face :(
|
||||
//
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates how many matches are associated with each base function in
|
||||
* the given result set and stores them in the {@link DisplayFunctionsPanel#functionMatchMap}.
|
||||
*
|
||||
* @param queryResult the object that holds the function matches
|
||||
*/
|
||||
private void parseFunctionMatchCounts(SFQueryResult queryResult) {
|
||||
|
||||
List<SimilarityResult> results = queryResult.getSimilarityResults();
|
||||
|
||||
for (SimilarityResult result : results) {
|
||||
String funcName = result.getBase().getFunctionName();
|
||||
functionMatchMap.put(funcName, result.getTotalCount());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Column for showing the number of matches each base function has.
|
||||
*
|
||||
* To make this as fast as possible, the counts for each function are NOT determined
|
||||
* here; they're calculated when a new result set is received and stored in the
|
||||
* {@link DisplayFunctionsPanel#functionMatchMap}. This class has only to
|
||||
* go to that map and extract the correct value.
|
||||
*
|
||||
* @see DisplayFunctionsPanel#parseFunctionMatchCounts(SFQueryResult)
|
||||
*
|
||||
*/
|
||||
private class MatchCountTableColumn
|
||||
extends AbstractProgramBasedDynamicTableColumn<Function, Integer> {
|
||||
|
||||
@Override
|
||||
public String getColumnName() {
|
||||
return "Matches";
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getValue(Function rowObject, Settings settings, Program program,
|
||||
ServiceProvider serviceProvider) throws IllegalArgumentException {
|
||||
|
||||
if (functionMatchMap == null) {
|
||||
return 0;
|
||||
}
|
||||
return functionMatchMap.get(rowObject.getName());
|
||||
}
|
||||
}
|
||||
}
|
@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@ -125,5 +125,6 @@ class VersionInfoPanel extends JPanel {
|
||||
private void addJavaInfo(List<String> lines, String def) {
|
||||
lines.add("Java Vendor: " + System.getProperty("java.vendor", def));
|
||||
lines.add("Java Version: " + System.getProperty("java.version", def));
|
||||
lines.add("Java Home: " + System.getProperty("java.home"));
|
||||
}
|
||||
}
|
||||
|
@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@ -21,13 +21,11 @@ import java.util.List;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.Border;
|
||||
import javax.swing.event.HyperlinkEvent;
|
||||
import javax.swing.event.HyperlinkListener;
|
||||
|
||||
import docking.DockingWindowManager;
|
||||
import docking.ReusableDialogComponentProvider;
|
||||
import docking.tool.ToolConstants;
|
||||
import docking.widgets.HyperlinkComponent;
|
||||
import docking.widgets.GHyperlinkComponent;
|
||||
import docking.widgets.checkbox.GCheckBox;
|
||||
import docking.widgets.combobox.GhidraComboBox;
|
||||
import ghidra.GhidraOptions;
|
||||
@ -77,7 +75,7 @@ public class GoToAddressLabelDialog extends ReusableDialogComponentProvider
|
||||
|
||||
private Navigatable navigatable;
|
||||
|
||||
private HyperlinkComponent hyperlink;
|
||||
private GHyperlinkComponent hyperlink;
|
||||
|
||||
private JCheckBox includeDynamicBox;
|
||||
|
||||
@ -183,23 +181,26 @@ public class GoToAddressLabelDialog extends ReusableDialogComponentProvider
|
||||
gbc.weightx = 1;
|
||||
gbc.gridwidth = 2;
|
||||
gbc.insets = new Insets(5, 5, 5, 5);
|
||||
hyperlink = new HyperlinkComponent("<html>Enter an address, label, <a href=\"" +
|
||||
EXPRESSION_ANCHOR_NAME + "\">expression</a>, or " + "<a href=\"" +
|
||||
FILE_OFFSET_ANCHOR_NAME + "\">file offset</a>:");
|
||||
HyperlinkListener hyperlinkListener = evt -> {
|
||||
if (evt.getEventType() == HyperlinkEvent.EventType.ACTIVATED) {
|
||||
HelpLocation loc = new HelpLocation(HelpTopics.NAVIGATION, evt.getDescription());
|
||||
DockingWindowManager.getHelpService().showHelp(loc);
|
||||
}
|
||||
};
|
||||
hyperlink.addHyperlinkListener(EXPRESSION_ANCHOR_NAME, hyperlinkListener);
|
||||
hyperlink.addHyperlinkListener(FILE_OFFSET_ANCHOR_NAME, hyperlinkListener);
|
||||
|
||||
hyperlink = new GHyperlinkComponent();
|
||||
hyperlink.addText("Enter an address, label, ");
|
||||
hyperlink.addLink("expression,", () -> {
|
||||
HelpLocation loc = new HelpLocation(HelpTopics.NAVIGATION, EXPRESSION_ANCHOR_NAME);
|
||||
DockingWindowManager.getHelpService().showHelp(loc);
|
||||
});
|
||||
|
||||
hyperlink.addText(" or ");
|
||||
hyperlink.addLink("file offset:", () -> {
|
||||
HelpLocation loc = new HelpLocation(HelpTopics.NAVIGATION, FILE_OFFSET_ANCHOR_NAME);
|
||||
DockingWindowManager.getHelpService().showHelp(loc);
|
||||
});
|
||||
|
||||
inner.add(hyperlink, gbc);
|
||||
|
||||
comboBox = new GhidraComboBox<>();
|
||||
comboBox.setEditable(true);
|
||||
comboBox.addActionListener(evt -> okCallback());
|
||||
String comboName = "Go To Address or Lable Text Field / Combobox";
|
||||
String comboName = "Go To Address or Label Text Field / Combobox";
|
||||
comboBox.setName(comboName);
|
||||
comboBox.getAccessibleContext().setAccessibleName(comboName);
|
||||
|
||||
@ -218,7 +219,7 @@ public class GoToAddressLabelDialog extends ReusableDialogComponentProvider
|
||||
inner.add(caseSensitiveBox, gbc);
|
||||
|
||||
includeDynamicBox = new GCheckBox("Dynamic labels", true);
|
||||
includeDynamicBox.setToolTipText("Include dynamic lables in the search (slower)");
|
||||
includeDynamicBox.setToolTipText("Include dynamic labels in the search (slower)");
|
||||
gbc.gridx = 1;
|
||||
inner.add(includeDynamicBox, gbc);
|
||||
String dynamicCheckBoxName = "Dynamic Checkbox";
|
||||
|
@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@ -16,21 +16,17 @@
|
||||
package ghidra.framework.main;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.HyperlinkEvent;
|
||||
import javax.swing.text.View;
|
||||
|
||||
import docking.DockingUtils;
|
||||
import docking.widgets.*;
|
||||
import docking.widgets.MultiLineLabel;
|
||||
import docking.widgets.label.*;
|
||||
import generic.theme.GColor;
|
||||
import generic.theme.GThemeDefaults.Colors.Palette;
|
||||
import generic.theme.Gui;
|
||||
import generic.util.WindowUtilities;
|
||||
import ghidra.framework.Application;
|
||||
import ghidra.framework.ApplicationProperties;
|
||||
import ghidra.util.*;
|
||||
@ -140,7 +136,6 @@ class InfoPanel extends JPanel {
|
||||
JPanel vPanel = new JPanel(new BorderLayout());
|
||||
vPanel.setBackground(bgColor);
|
||||
vPanel.add(buildVersionLabel(), BorderLayout.CENTER);
|
||||
vPanel.add(buildJavaVersionComponent(), BorderLayout.SOUTH);
|
||||
return vPanel;
|
||||
}
|
||||
|
||||
@ -192,41 +187,6 @@ class InfoPanel extends JPanel {
|
||||
return imagePanel;
|
||||
}
|
||||
|
||||
private HyperlinkComponent buildJavaVersionComponent() {
|
||||
String anchorName = "java_version";
|
||||
final HyperlinkComponent javaVersionComponent =
|
||||
new HyperlinkComponent("<html><CENTER>Java Version " + "<A HREF=\"" + anchorName +
|
||||
"\">" + System.getProperty("java.version") + "</A></CENTER>");
|
||||
javaVersionComponent.addHyperlinkListener(anchorName, e -> {
|
||||
if (e.getEventType() != HyperlinkEvent.EventType.ACTIVATED) {
|
||||
return;
|
||||
}
|
||||
|
||||
showJavaHomeInfo(javaVersionComponent);
|
||||
});
|
||||
|
||||
DockingUtils.setTransparent(javaVersionComponent);
|
||||
return javaVersionComponent;
|
||||
}
|
||||
|
||||
private void showJavaHomeInfo(final HyperlinkComponent javaVersionComponent) {
|
||||
JToolTip tooltip = new JToolTip();
|
||||
tooltip.setTipText(System.getProperty("java.home"));
|
||||
|
||||
Point location = MouseInfo.getPointerInfo().getLocation();
|
||||
Window window = WindowUtilities.windowForComponent(javaVersionComponent);
|
||||
tooltip.setLocation(location);
|
||||
|
||||
PopupWindow popupWindow = new PopupWindow(window, tooltip);
|
||||
|
||||
SwingUtilities.convertPointFromScreen(location, javaVersionComponent);
|
||||
MouseEvent dummyEvent =
|
||||
new MouseEvent(javaVersionComponent, (int) System.currentTimeMillis(),
|
||||
System.currentTimeMillis(), 0, location.x, location.y, 1, false);
|
||||
popupWindow.setCloseWindowDelay(1);
|
||||
popupWindow.showPopup(dummyEvent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the version information from the the resource file.
|
||||
*/
|
||||
@ -241,7 +201,8 @@ class InfoPanel extends JPanel {
|
||||
// set some default values in case we don't have the resource file.
|
||||
version = "Version " + Application.getApplicationVersion() +
|
||||
(SystemUtilities.isInDevelopmentMode() ? " - DEVELOPMENT" : "") + buildInfo + "\n" +
|
||||
Application.getBuildDate();
|
||||
Application.getBuildDate() + "\n" +
|
||||
"Java Version " + System.getProperty("java.version");
|
||||
|
||||
marking =
|
||||
Application.getApplicationProperty(ApplicationProperties.RELEASE_MARKING_PROPERTY);
|
||||
|
@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@ -46,6 +46,7 @@ import ghidra.util.filechooser.GhidraFileFilter;
|
||||
import ghidra.util.xml.GenericXMLOutputter;
|
||||
import ghidra.util.xml.XmlUtilities;
|
||||
import util.CollectionUtils;
|
||||
import utilities.util.reflection.ReflectionUtilities;
|
||||
|
||||
/**
|
||||
* A class to provide utilities for system key bindings, such as importing and
|
||||
@ -340,6 +341,12 @@ public class KeyBindingUtils {
|
||||
if (keyText == null) {
|
||||
// no binding--just pick a name
|
||||
keyText = action.getValue(Action.NAME);
|
||||
if (keyText == null) {
|
||||
Msg.error(KeyBindingUtils.class, "Action must have a name to be registered",
|
||||
ReflectionUtilities.createJavaFilteredThrowable());
|
||||
return;
|
||||
}
|
||||
|
||||
im.put(keyStroke, keyText);
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,224 @@
|
||||
/* ###
|
||||
* 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.widgets;
|
||||
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.Border;
|
||||
import javax.swing.event.HyperlinkEvent;
|
||||
import javax.swing.event.HyperlinkEvent.EventType;
|
||||
import javax.swing.event.HyperlinkListener;
|
||||
import javax.swing.text.DefaultCaret;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
|
||||
import docking.DockingUtils;
|
||||
import docking.actions.KeyBindingUtils;
|
||||
import generic.theme.GColor;
|
||||
import ghidra.docking.util.LookAndFeelUtils;
|
||||
import utility.function.Callback;
|
||||
|
||||
/**
|
||||
* A component that acts like a label, but adds the ability to render HTML links with a client
|
||||
* callback for when the link is activated. Links can be activated by mouse clicking or or by
|
||||
* focusing the link and then pressing Enter or Space.
|
||||
* <p>
|
||||
* Users can make one simple text link by calling {@link #addLink(String, Callback)}.
|
||||
* Alternatively, users can mix plain text and links by using both {@link #addText(String)} and
|
||||
* {@link #addLink(String, Callback)}.
|
||||
*/
|
||||
public class GHyperlinkComponent extends JPanel {
|
||||
|
||||
public GHyperlinkComponent() {
|
||||
setLayout(new BoxLayout(this, BoxLayout.LINE_AXIS));
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds text to this widget that will be displayed as plain text.
|
||||
* @param text the text
|
||||
*/
|
||||
public void addText(String text) {
|
||||
JTextPane textPane = new FixedSizeTextPane(text, Callback.dummy());
|
||||
textPane.setFocusable(false);
|
||||
add(textPane);
|
||||
String updated = text;
|
||||
|
||||
// the text pane will trim leading spaces; keep the client spaces
|
||||
int leadingSpaces = text.indexOf(text.trim());
|
||||
if (leadingSpaces != 0) {
|
||||
updated = StringUtils.repeat(" ", leadingSpaces) + text.substring(leadingSpaces);
|
||||
}
|
||||
|
||||
setText(textPane, updated);
|
||||
|
||||
// clear the description to avoid excess text reading
|
||||
textPane.getAccessibleContext().setAccessibleDescription("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses the given text to create a link the user can click.
|
||||
* @param text the text
|
||||
* @param linkActivatedCallback the callback that will be called when the link is activated
|
||||
*/
|
||||
public void addLink(String text, Callback linkActivatedCallback) {
|
||||
JTextPane textPane = new FixedSizeTextPane(text, linkActivatedCallback);
|
||||
add(textPane);
|
||||
setText(textPane, "<a href=\"stub\">" + text + "</a>");
|
||||
textPane.getAccessibleContext().setAccessibleDescription("Clickable link");
|
||||
}
|
||||
|
||||
private void setText(JTextPane textPane, String text) {
|
||||
|
||||
String html = "<html><nobr>" + text;
|
||||
textPane.setText(html);
|
||||
|
||||
//
|
||||
// Hack Alert!: We've run into scenarios where changing the text of this component
|
||||
// causes the entire component to paint no text. For some reason, the
|
||||
// component will work correctly if it has a non-zero size border installed.
|
||||
// Also, if we call getPreferredSize(), then it will work.
|
||||
//
|
||||
textPane.getPreferredSize();
|
||||
getPreferredSize();
|
||||
}
|
||||
|
||||
//==================================================================================================
|
||||
// Inner Classes
|
||||
//==================================================================================================
|
||||
|
||||
/**
|
||||
* A text pane that can render links.
|
||||
*/
|
||||
private class FixedSizeTextPane extends JTextPane {
|
||||
|
||||
private String rawText;
|
||||
|
||||
FixedSizeTextPane(String rawText, Callback linkActivatedCallback) {
|
||||
this.rawText = rawText;
|
||||
|
||||
setContentType("text/html");
|
||||
setEditable(false);
|
||||
setCaret(new NonScrollingCaret());
|
||||
DockingUtils.setTransparent(this);
|
||||
|
||||
// Create a border the same dimensions as the one we will later switch to when focused.
|
||||
// This keeps the UI from moving.
|
||||
Border defaultBorder = BorderFactory.createEmptyBorder(1, 1, 1, 1);
|
||||
setBorder(defaultBorder);
|
||||
|
||||
// change the border of the link so users can see when it is focused
|
||||
Color FOCUS_COLOR = new GColor("color.border.button.focused");
|
||||
Border FOCUSED_BORDER = BorderFactory.createLineBorder(FOCUS_COLOR);
|
||||
addFocusListener(new FocusListener() {
|
||||
|
||||
@Override
|
||||
public void focusLost(FocusEvent e) {
|
||||
setBorder(defaultBorder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void focusGained(FocusEvent e) {
|
||||
setBorder(FOCUSED_BORDER);
|
||||
}
|
||||
});
|
||||
|
||||
addHyperlinkListener(new HyperlinkListener() {
|
||||
@Override
|
||||
public void hyperlinkUpdate(HyperlinkEvent e) {
|
||||
EventType type = e.getEventType();
|
||||
if (type == EventType.ACTIVATED) {
|
||||
linkActivatedCallback.call();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
addActivationKeyBinding(linkActivatedCallback);
|
||||
}
|
||||
|
||||
private void addActivationKeyBinding(Callback linkActivatedCallback) {
|
||||
// Space and Enter typically activate buttons
|
||||
KeyStroke enterKs = KeyBindingUtils.parseKeyStroke("Enter");
|
||||
KeyStroke spaceKs = KeyBindingUtils.parseKeyStroke("Space");
|
||||
Action action = new AbstractAction("Activate Link") {
|
||||
@Override
|
||||
public void actionPerformed(ActionEvent e) {
|
||||
linkActivatedCallback.call();
|
||||
}
|
||||
};
|
||||
KeyBindingUtils.registerAction(this, enterKs, action, JComponent.WHEN_FOCUSED);
|
||||
KeyBindingUtils.registerAction(this, spaceKs, action, JComponent.WHEN_FOCUSED);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getMaximumSize() {
|
||||
return getBestSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getMinimumSize() {
|
||||
return getBestSize();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Dimension getPreferredSize() {
|
||||
return getBestSize();
|
||||
}
|
||||
|
||||
private Dimension getBestSize() {
|
||||
int width = getBestWidth();
|
||||
Dimension d = super.getPreferredSize();
|
||||
d.width = Math.min(width, d.width);
|
||||
return d;
|
||||
}
|
||||
|
||||
private int getBestWidth() {
|
||||
Font font = getFont();
|
||||
FontMetrics fm = getFontMetrics(font);
|
||||
int stringWidth = fm.stringWidth(rawText);
|
||||
|
||||
//
|
||||
// Make a width that is at least as big as the width of the text. Use the preferred
|
||||
// width, as that is more accurate. Do not do this for the FlatLaf UIs because their
|
||||
// preferred width includes a minimum width which will be too large for a small number
|
||||
// of characters.
|
||||
//
|
||||
Dimension preferred = super.getPreferredSize();
|
||||
int width = stringWidth;
|
||||
if (!LookAndFeelUtils.isUsingFlatUI()) {
|
||||
width = Math.max(stringWidth, preferred.width);
|
||||
}
|
||||
|
||||
// a fudge factor to compensate for text calculation rounding, based on a 12pt font size
|
||||
Insets insets = getInsets();
|
||||
width += insets.left + insets.right;
|
||||
return width;
|
||||
}
|
||||
}
|
||||
|
||||
private class NonScrollingCaret extends DefaultCaret {
|
||||
|
||||
private NonScrollingCaret() {
|
||||
setVisible(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void adjustVisibility(Rectangle nloc) {
|
||||
// we don't want to adjust any visibility (no scrolling for this comp)
|
||||
}
|
||||
}
|
||||
}
|
@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@ -15,18 +15,20 @@
|
||||
*/
|
||||
package docking.widgets;
|
||||
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Rectangle;
|
||||
import java.awt.event.MouseListener;
|
||||
import java.awt.event.MouseMotionListener;
|
||||
import java.util.*;
|
||||
import java.awt.*;
|
||||
import java.awt.event.*;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.border.Border;
|
||||
import javax.swing.event.HyperlinkEvent;
|
||||
import javax.swing.event.HyperlinkListener;
|
||||
import javax.swing.text.DefaultCaret;
|
||||
|
||||
import docking.DockingUtils;
|
||||
import generic.theme.GColor;
|
||||
|
||||
/**
|
||||
* A component that acts like a label, but adds the ability to render HTML anchors and the
|
||||
@ -37,7 +39,9 @@ import docking.DockingUtils;
|
||||
* this component will display the hyperlinks properly and will notify any registered
|
||||
* listeners ({@link #addHyperlinkListener(String, HyperlinkListener)} that the user has clicked the link
|
||||
* by the given name.
|
||||
* @deprecated Replaced by {@link GHyperlinkComponent}
|
||||
*/
|
||||
@Deprecated(since = "11.3", forRemoval = true)
|
||||
public class HyperlinkComponent extends JPanel {
|
||||
|
||||
private JTextPane textPane;
|
||||
@ -55,6 +59,7 @@ public class HyperlinkComponent extends JPanel {
|
||||
textPane.setCaret(new NonScrollingCaret());
|
||||
|
||||
textPane.addHyperlinkListener(new HyperlinkListener() {
|
||||
|
||||
@Override
|
||||
public void hyperlinkUpdate(HyperlinkEvent e) {
|
||||
String anchorText = e.getDescription();
|
||||
@ -84,6 +89,22 @@ public class HyperlinkComponent extends JPanel {
|
||||
textPane.setPreferredSize(getPreferredSize());
|
||||
|
||||
hyperlinkListeners = new HashMap<String, List<HyperlinkListener>>();
|
||||
|
||||
Color FOCUS_COLOR = new GColor("color.border.button.focused");
|
||||
Border FOCUSED_BORDER = BorderFactory.createLineBorder(FOCUS_COLOR);
|
||||
Border defaultBorder = textPane.getBorder();
|
||||
textPane.addFocusListener(new FocusListener() {
|
||||
|
||||
@Override
|
||||
public void focusLost(FocusEvent e) {
|
||||
textPane.setBorder(defaultBorder);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void focusGained(FocusEvent e) {
|
||||
textPane.setBorder(FOCUSED_BORDER);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@ -159,6 +180,11 @@ public class HyperlinkComponent extends JPanel {
|
||||
setVisible(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVisible(boolean e) {
|
||||
super.setVisible(e);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void adjustVisibility(Rectangle nloc) {
|
||||
// we don't want to adjust any visibility (no scrolling for this comp)
|
||||
|
@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@ -66,7 +66,7 @@ public class LookAndFeelUtils {
|
||||
* @return true if the UI is using Aqua
|
||||
*/
|
||||
public static boolean isUsingAquaUI(ComponentUI UI) {
|
||||
return ThemeManager.getInstance().isUsingAquaUI(UI);
|
||||
return ThemeManager.getInstance().isUsingAquaUI();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -77,4 +77,11 @@ public class LookAndFeelUtils {
|
||||
return ThemeManager.getInstance().isUsingNimbusUI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current UI is the FlatLaf Dark or FlatLaf Light Look and Feel.
|
||||
* @return true if the current UI is the FlatLaf Dark or FlatLaf Light Look and Feel
|
||||
*/
|
||||
public static boolean isUsingFlatUI() {
|
||||
return ThemeManager.getInstance().isUsingFlatUI();
|
||||
}
|
||||
}
|
||||
|
@ -490,13 +490,32 @@ public abstract class ThemeManager {
|
||||
|
||||
/**
|
||||
* Returns true if the given UI object is using the Aqua Look and Feel.
|
||||
* @param UI the UI to examine.
|
||||
* @param UI the UI to examine. (This parameter is ignored)
|
||||
* @return true if the UI is using Aqua
|
||||
* @deprecated use {@link #isUsingAquaUI()}
|
||||
*/
|
||||
@Deprecated(since = "11.3", forRemoval = true)
|
||||
public boolean isUsingAquaUI(ComponentUI UI) {
|
||||
return getLookAndFeelType() == LafType.MAC;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current UI is using the Aqua Look and Feel.
|
||||
* @return true if the UI is using Aqua
|
||||
*/
|
||||
public boolean isUsingAquaUI() {
|
||||
return getLookAndFeelType() == LafType.MAC;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the current UI is the FlatLaf Dark or FlatLaf Light Look and Feel.
|
||||
* @return true if the current UI is the FlatLaf Dark or FlatLaf Light Look and Feel
|
||||
*/
|
||||
public boolean isUsingFlatUI() {
|
||||
return getLookAndFeelType() == LafType.FLAT_DARK ||
|
||||
getLookAndFeelType() == LafType.FLAT_LIGHT;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if 'Nimbus' is the current Look and Feel
|
||||
* @return true if 'Nimbus' is the current Look and Feel
|
||||
@ -618,7 +637,7 @@ public abstract class ThemeManager {
|
||||
OperatingSystem OS = Platform.CURRENT_PLATFORM.getOperatingSystem();
|
||||
return switch (OS) {
|
||||
case MAC_OS_X -> new MacTheme();
|
||||
case WINDOWS -> new WindowsTheme();
|
||||
case WINDOWS -> new WindowsTheme();
|
||||
default -> new FlatLightTheme();
|
||||
};
|
||||
}
|
||||
|
@ -4,9 +4,9 @@
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
@ -20,10 +20,9 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import javax.swing.*;
|
||||
import javax.swing.event.HyperlinkEvent.EventType;
|
||||
|
||||
import docking.EmptyBorderToggleButton;
|
||||
import docking.widgets.HyperlinkComponent;
|
||||
import docking.widgets.GHyperlinkComponent;
|
||||
import docking.widgets.checkbox.GCheckBox;
|
||||
import docking.widgets.label.*;
|
||||
import generic.theme.*;
|
||||
@ -180,7 +179,7 @@ public class PluginManagerComponent extends JPanel implements Scrollable {
|
||||
nameLabel.setForeground(new GColor("color.fg.pluginpanel.name"));
|
||||
labelPanel.add(nameLabel);
|
||||
|
||||
HyperlinkComponent configureHyperlink = createConfigureHyperlink();
|
||||
GHyperlinkComponent configureHyperlink = createConfigureHyperlink();
|
||||
labelPanel.add(configureHyperlink);
|
||||
|
||||
labelPanel.setBorder(BorderFactory.createEmptyBorder(0, 25, 0, 40));
|
||||
@ -188,14 +187,13 @@ public class PluginManagerComponent extends JPanel implements Scrollable {
|
||||
add(centerPanel);
|
||||
}
|
||||
|
||||
private HyperlinkComponent createConfigureHyperlink() {
|
||||
HyperlinkComponent configureHyperlink =
|
||||
new HyperlinkComponent("<html> <a href=\"Configure\">Configure</a>");
|
||||
configureHyperlink.addHyperlinkListener("Configure", e -> {
|
||||
if (e.getEventType() == EventType.ACTIVATED) {
|
||||
managePlugins(PluginPackageComponent.this.pluginPackage);
|
||||
}
|
||||
private GHyperlinkComponent createConfigureHyperlink() {
|
||||
GHyperlinkComponent configureHyperlink =
|
||||
new GHyperlinkComponent();
|
||||
configureHyperlink.addLink("Configure", () -> {
|
||||
managePlugins(PluginPackageComponent.this.pluginPackage);
|
||||
});
|
||||
|
||||
configureHyperlink.setBackground(BG);
|
||||
return configureHyperlink;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user