diff --git a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/gui/search/results/DisplayFunctionsPanel.java b/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/gui/search/results/DisplayFunctionsPanel.java deleted file mode 100755 index 328acd1f66..0000000000 --- a/Ghidra/Features/BSim/src/main/java/ghidra/features/bsim/gui/search/results/DisplayFunctionsPanel.java +++ /dev/null @@ -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 selectedFunctions; - - // Maps input functions to the number of matches associated with it. This is - // here to provide quick access for the MatchCountTableColumn. - private Map 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( - "No functions selected"); - return buffy.toString(); - } - - void close() { - if (tableProvider != null) { - tableProvider.removeFromTool(); - tableProvider = null; - } - - selectedFunctions = null; - } - - void setSelectedFunctions(Set 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 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(" "); // open anchor - buffy.append(""); - buffy.append(" (show table) "); - buffy.append(""); - buffy.append(""); // 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 { - private final List functions; - - SelectedFunctionsModel(Program program, ServiceProvider serviceProvider, - Set functions) { - super("Selected Query Functions", serviceProvider, program, null); - this.functions = new ArrayList<>(functions); - } - - @Override - protected TableColumnDescriptor createTableColumnDescriptor() { - TableColumnDescriptor 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 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 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 { - - @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()); - } - } -} diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/runtimeinfo/VersionInfoPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/runtimeinfo/VersionInfoPanel.java index 252db04046..39246bb705 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/runtimeinfo/VersionInfoPanel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/runtimeinfo/VersionInfoPanel.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -125,5 +125,6 @@ class VersionInfoPanel extends JPanel { private void addJavaInfo(List 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")); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/util/navigation/GoToAddressLabelDialog.java b/Ghidra/Features/Base/src/main/java/ghidra/app/util/navigation/GoToAddressLabelDialog.java index 038b977620..1d5eba80fa 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/util/navigation/GoToAddressLabelDialog.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/util/navigation/GoToAddressLabelDialog.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -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("Enter an address, label, expression, or " + "file offset:"); - 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"; diff --git a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/InfoPanel.java b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/InfoPanel.java index 0578913623..f90cad54c0 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/framework/main/InfoPanel.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/framework/main/InfoPanel.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -16,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("
Java Version " + "" + System.getProperty("java.version") + "
"); - 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); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/actions/KeyBindingUtils.java b/Ghidra/Framework/Docking/src/main/java/docking/actions/KeyBindingUtils.java index b166930784..e36d02be5d 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/actions/KeyBindingUtils.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/actions/KeyBindingUtils.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -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); } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/GHyperlinkComponent.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/GHyperlinkComponent.java new file mode 100644 index 0000000000..beab7374a9 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/GHyperlinkComponent.java @@ -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. + *

+ * 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, "" + text + ""); + textPane.getAccessibleContext().setAccessibleDescription("Clickable link"); + } + + private void setText(JTextPane textPane, String text) { + + String html = "" + 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) + } + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/HyperlinkComponent.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/HyperlinkComponent.java index 2bffed45bc..52ffc8b50e 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/HyperlinkComponent.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/HyperlinkComponent.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -15,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>(); + + 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) diff --git a/Ghidra/Framework/Docking/src/main/java/ghidra/docking/util/LookAndFeelUtils.java b/Ghidra/Framework/Docking/src/main/java/ghidra/docking/util/LookAndFeelUtils.java index 410efd1450..2d7fd1464c 100644 --- a/Ghidra/Framework/Docking/src/main/java/ghidra/docking/util/LookAndFeelUtils.java +++ b/Ghidra/Framework/Docking/src/main/java/ghidra/docking/util/LookAndFeelUtils.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -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(); + } } diff --git a/Ghidra/Framework/Gui/src/main/java/generic/theme/ThemeManager.java b/Ghidra/Framework/Gui/src/main/java/generic/theme/ThemeManager.java index 8f6f0d8f36..6c324b6132 100644 --- a/Ghidra/Framework/Gui/src/main/java/generic/theme/ThemeManager.java +++ b/Ghidra/Framework/Gui/src/main/java/generic/theme/ThemeManager.java @@ -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(); }; } diff --git a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/PluginManagerComponent.java b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/PluginManagerComponent.java index 7cd36dfb0e..045434ca75 100644 --- a/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/PluginManagerComponent.java +++ b/Ghidra/Framework/Project/src/main/java/ghidra/framework/plugintool/dialog/PluginManagerComponent.java @@ -4,9 +4,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,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(" Configure"); - 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; }