GP-5049 - Refactor of hyperlink component to support accessibility

This commit is contained in:
dragonmacher 2024-11-07 12:31:55 -05:00
parent aaa19420e9
commit 8a6145e2df
10 changed files with 335 additions and 376 deletions

View File

@ -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());
}
}
}

View File

@ -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"));
}
}

View File

@ -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";

View File

@ -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);

View File

@ -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);
}

View File

@ -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("&nbsp;", 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)
}
}
}

View File

@ -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)

View File

@ -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();
}
}

View File

@ -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();
};
}

View File

@ -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;
}