diff --git a/Ghidra/Features/Base/data/base.theme.properties b/Ghidra/Features/Base/data/base.theme.properties index bec484b2a6..d267133b43 100644 --- a/Ghidra/Features/Base/data/base.theme.properties +++ b/Ghidra/Features/Base/data/base.theme.properties @@ -37,6 +37,11 @@ color.bg.markerservice = color.bg color.bg.search.highlight = rgb(255,255,200) color.bg.search.current-line.highlight = yellow +color.bg.tree.renderer.icon.fill = #9F9FFF +color.bg.tree.renderer.icon.line = #8282FF + + + [Dark Defaults] color.bg = rgb(40, 42, 46) // TODO this should be in a more generic module diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/tree/BackgroundIcon.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/tree/BackgroundIcon.java index 623bf08c0f..22a8cae0fd 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/tree/BackgroundIcon.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/tree/BackgroundIcon.java @@ -19,10 +19,15 @@ import java.awt.*; import javax.swing.Icon; +import docking.theme.GColor; +import docking.theme.GThemeDefaults.Colors.Palette; + class BackgroundIcon implements Icon { - private static Color VERSION_ICON_COLOR_DARK = new Color(0x82, 0x82, 0xff); - private static Color VERSION_ICON_COLOR_LIGHT = new Color(0x9f, 0x9f, 0xff); + private static Color VERSION_ICON_COLOR_LINE = new GColor("color.bg.tree.renderer.icon.line"); + private static Color VERSION_ICON_COLOR_LIGHT = new GColor("color.bg.tree.renderer.icon.fill"); + + private static Color ALPHA = Palette.NO_COLOR; private int width; private int height; @@ -49,14 +54,14 @@ class BackgroundIcon implements Icon { if (isVersioned) { g.setColor(VERSION_ICON_COLOR_LIGHT); g.fillRect(x + 1, y + 1, width - 2, height - 2); - g.setColor(VERSION_ICON_COLOR_DARK); + g.setColor(VERSION_ICON_COLOR_LINE); g.drawLine(x + 1, y, x + width - 2, y); g.drawLine(x + width - 1, y + 1, x + width - 1, y + height - 2); g.drawLine(x + 1, y + height - 1, x + width - 2, y + height - 1); g.drawLine(x, y + 1, x, y + height - 2); } else { - g.setColor(c.getBackground()); + g.setColor(ALPHA); g.fillRect(x, y, width, height); } } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/tree/DataTypeArchiveGTree.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/tree/DataTypeArchiveGTree.java index 7e40136a78..e6adf7b94f 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/tree/DataTypeArchiveGTree.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/core/datamgr/tree/DataTypeArchiveGTree.java @@ -335,7 +335,7 @@ public class DataTypeArchiveGTree extends GTree { // work around an issue on some platforms where the label is painting a color that // does not match the tree label.setBackground( - isSelected ? getBackgroundSelectionColor() : tree.getBackground()); + isSelected ? getBackgroundSelectionColor() : getBackgroundNonSelectionColor()); } MultiIcon multiIcon = new MultiIcon( @@ -362,7 +362,6 @@ public class DataTypeArchiveGTree extends GTree { } setIcon(multiIcon); - return label; } diff --git a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/gui/ThemeManagerPlugin.java b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/gui/ThemeManagerPlugin.java index 67cc09949d..c5b860130d 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/gui/ThemeManagerPlugin.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/app/plugin/gui/ThemeManagerPlugin.java @@ -15,23 +15,15 @@ */ package ghidra.app.plugin.gui; -import java.util.*; -import java.util.stream.Collectors; - import docking.action.builder.ActionBuilder; -import docking.options.editor.StringWithChoicesEditor; -import docking.theme.GTheme; -import docking.theme.Gui; -import docking.theme.gui.GThemeDialog; -import docking.tool.ToolConstants; +import docking.theme.gui.ThemeDialog; import ghidra.app.CorePluginPackage; import ghidra.app.plugin.PluginCategoryNames; import ghidra.framework.main.FrontEndOnly; import ghidra.framework.main.FrontEndTool; -import ghidra.framework.options.*; import ghidra.framework.plugintool.*; import ghidra.framework.plugintool.util.PluginStatus; -import ghidra.util.*; +import ghidra.util.SystemUtilities; //@formatter:off @PluginInfo( @@ -43,93 +35,26 @@ import ghidra.util.*; "This plugin is available only in the Ghidra Project Window." ) //@formatter:on -public class ThemeManagerPlugin extends Plugin implements FrontEndOnly, OptionsChangeListener { - - public final static String THEME_OPTIONS_NAME = "Theme"; - private final static String OPTIONS_TITLE = ToolConstants.TOOL_OPTIONS; - - private boolean issuedRestartNotification; -// private static boolean issuedPreferredDarkThemeLafNotification; +public class ThemeManagerPlugin extends Plugin implements FrontEndOnly { public ThemeManagerPlugin(PluginTool tool) { super(tool); SystemUtilities.assertTrue(tool instanceof FrontEndTool, "Plugin added to the wrong type of tool"); - initThemeOptions(); } @Override protected void init() { - new ActionBuilder("Show Properties", getName()).menuPath("Edit", "Theme Properties") + new ActionBuilder("", getName()).menuPath("Edit", "Theme") .onAction(e -> showThemeProperties()) .buildAndInstall(tool); } private void showThemeProperties() { - GThemeDialog dialog = new GThemeDialog(); - tool.showDialog(dialog); + ThemeDialog.editTheme(); } - private void initThemeOptions() { - - ToolOptions opt = tool.getOptions(OPTIONS_TITLE); - - GTheme activeTheme = Gui.getActiveTheme(); - Set themes = Gui.getSupportedThemes(); - List themeNames = - themes.stream().map(t -> t.getName()).collect(Collectors.toList()); - Collections.sort(themeNames); - - opt.registerOption(THEME_OPTIONS_NAME, OptionType.STRING_TYPE, activeTheme.getName(), - new HelpLocation(ToolConstants.TOOL_HELP_TOPIC, "Look_And_Feel"), - "Set the look and feel for Ghidra. After you change the " + - "look and feel, you will have to restart Ghidra to see the effect.", - new StringWithChoicesEditor(themeNames)); - - opt.addOptionsChangeListener(this); - } - - @Override - public void optionsChanged(ToolOptions options, String optionName, Object oldValue, - Object newValue) { - - if (optionName.equals(THEME_OPTIONS_NAME)) { - String newThemeName = (String) newValue; - if (!newThemeName.equals(Gui.getActiveTheme().getName())) { - issueRestartNeededMessage(); - } - - saveLookAndFeel((String) newValue); - } - - } - - private void saveLookAndFeel(String themeName) { - Set allThemes = Gui.getAllThemes(); - for (GTheme theme : allThemes) { - if (theme.getName().equals(themeName)) { - Gui.saveThemeToPreferneces(theme); - } - } - } - - private void issueRestartNeededMessage() { - if (issuedRestartNotification) { - return; - } - - issuedRestartNotification = true; - Msg.showInfo(getClass(), null, "Look And Feel Updated", - "The new Look and Feel will take effect \nafter you exit and restart Ghidra."); - } - - @Override - public void dispose() { - ToolOptions opt = tool.getOptions(OPTIONS_TITLE); - opt.removeOptionsChangeListener(this); - super.dispose(); - } } diff --git a/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/FunctionGraphGroupVertices1Test.java b/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/FunctionGraphGroupVertices1Test.java index 7a267d333d..9eed8a03f8 100644 --- a/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/FunctionGraphGroupVertices1Test.java +++ b/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/FunctionGraphGroupVertices1Test.java @@ -26,6 +26,7 @@ import org.junit.*; import docking.ActionContext; import docking.action.DockingActionIf; +import docking.theme.GThemeDefaults.Colors.Palette; import edu.uci.ics.jung.graph.Graph; import ghidra.app.plugin.core.clear.ClearPlugin; import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; @@ -485,7 +486,7 @@ public class FunctionGraphGroupVertices1Test extends AbstractFunctionGraphTest { FGVertex v2 = vertex("01002d0f"); // color just one of the vertices - Color newColor = Color.RED; + Color newColor = Palette.RED; color(v1, newColor); GroupedFunctionGraphVertex group = group("A", v1, v2); @@ -527,7 +528,7 @@ public class FunctionGraphGroupVertices1Test extends AbstractFunctionGraphTest { FGVertex v2 = vertex("01002d0f"); // color just one of the vertices - Color newColor = Color.RED; + Color newColor = Palette.RED; color(v1, newColor); color(v2, newColor); @@ -564,7 +565,7 @@ public class FunctionGraphGroupVertices1Test extends AbstractFunctionGraphTest { // // Change the group color // - Color newGroupColor = Color.CYAN; + Color newGroupColor = Palette.CYAN; color(group, newGroupColor); // @@ -591,7 +592,7 @@ public class FunctionGraphGroupVertices1Test extends AbstractFunctionGraphTest { FGVertex v2 = vertex("01002d0f"); // color just one of the vertices - Color newColor = Color.RED; + Color newColor = Palette.RED; color(v1, newColor); GroupedFunctionGraphVertex group = group("A", v1, v2); @@ -599,7 +600,7 @@ public class FunctionGraphGroupVertices1Test extends AbstractFunctionGraphTest { // // Change the group color // - Color newGroupColor = Color.CYAN; + Color newGroupColor = Palette.CYAN; color(group, newGroupColor); // @@ -625,7 +626,7 @@ public class FunctionGraphGroupVertices1Test extends AbstractFunctionGraphTest { FGVertex v2 = vertex("01002d0f"); // color just one of the vertices - Color newColor = Color.RED; + Color newColor = Palette.RED; color(v1, newColor); color(v2, newColor); @@ -634,7 +635,7 @@ public class FunctionGraphGroupVertices1Test extends AbstractFunctionGraphTest { // // Change the group color // - Color newGroupColor = Color.CYAN; + Color newGroupColor = Palette.CYAN; color(group, newGroupColor); // @@ -663,7 +664,7 @@ public class FunctionGraphGroupVertices1Test extends AbstractFunctionGraphTest { // // Change the group color // - Color newGroupColor = Color.CYAN; + Color newGroupColor = Palette.CYAN; color(group, newGroupColor); // @@ -738,7 +739,7 @@ public class FunctionGraphGroupVertices1Test extends AbstractFunctionGraphTest { // // Color just one of the vertices // - Color newColor = Color.RED; + Color newColor = Palette.RED; color(v1, newColor); // @@ -782,7 +783,7 @@ public class FunctionGraphGroupVertices1Test extends AbstractFunctionGraphTest { // // Color just one of the vertices // - Color newColor = Color.RED; + Color newColor = Palette.RED; color(v1, newColor); // diff --git a/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/FunctionGraphPlugin1Test.java b/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/FunctionGraphPlugin1Test.java index c30e7044b1..8337052dc8 100644 --- a/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/FunctionGraphPlugin1Test.java +++ b/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/FunctionGraphPlugin1Test.java @@ -32,6 +32,7 @@ import docking.ActionContext; import docking.ComponentProvider; import docking.action.DockingAction; import docking.dnd.GClipboard; +import docking.theme.GThemeDefaults.Colors.Palette; import edu.uci.ics.jung.algorithms.layout.Layout; import edu.uci.ics.jung.visualization.VisualizationModel; import edu.uci.ics.jung.visualization.VisualizationViewer; @@ -619,7 +620,7 @@ public class FunctionGraphPlugin1Test extends AbstractFunctionGraphTest { Color appliedBackgroundColor = colorizingService.getBackgroundColor(focusedVertex.getVertexAddress()); - Color testColor = Color.RED; + Color testColor = Palette.RED; assertTrue("Unexpected start color--must change the test!", !testColor.equals(appliedBackgroundColor)); diff --git a/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/FunctionGraphPlugin2Test.java b/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/FunctionGraphPlugin2Test.java index 18a8872892..3bd74b9fac 100644 --- a/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/FunctionGraphPlugin2Test.java +++ b/Ghidra/Features/FunctionGraph/src/test.slow/java/ghidra/app/plugin/core/functiongraph/FunctionGraphPlugin2Test.java @@ -15,8 +15,7 @@ */ package ghidra.app.plugin.core.functiongraph; -import static ghidra.graph.viewer.GraphViewerUtils.getGraphScale; -import static ghidra.graph.viewer.GraphViewerUtils.getPointInViewSpaceForVertex; +import static ghidra.graph.viewer.GraphViewerUtils.*; import static org.junit.Assert.*; import java.awt.Color; @@ -28,6 +27,7 @@ import javax.swing.JComponent; import org.junit.*; import docking.action.DockingActionIf; +import docking.theme.GThemeDefaults.Colors.Palette; import edu.uci.ics.jung.graph.Graph; import generic.test.TestUtils; import ghidra.app.cmd.label.AddLabelCmd; @@ -251,7 +251,7 @@ public class FunctionGraphPlugin2Test extends AbstractFunctionGraphTest { ListingPanel listingPanel = (ListingPanel) TestUtils.getInstanceField("listingPanel", panel); Color startBackgrond = listingPanel.getTextBackgroundColor(); - Color testColor = Color.RED; + Color testColor = Palette.RED; assertTrue("Unexpected start color--must change the test!", !testColor.equals(startBackgrond)); @@ -277,7 +277,7 @@ public class FunctionGraphPlugin2Test extends AbstractFunctionGraphTest { Color appliedBackgroundColor = colorizingService.getBackgroundColor(vertex.getVertexAddress()); - Color testColor = Color.RED; + Color testColor = Palette.RED; assertTrue("Unexpected start color--must change the test!", !testColor.equals(appliedBackgroundColor)); diff --git a/Ghidra/Features/FunctionGraph/src/test/java/ghidra/app/plugin/core/functiongraph/FunctionGraphGroupVertices1Test.java b/Ghidra/Features/FunctionGraph/src/test/java/ghidra/app/plugin/core/functiongraph/FunctionGraphGroupVertices1Test.java deleted file mode 100644 index 9eed8a03f8..0000000000 --- a/Ghidra/Features/FunctionGraph/src/test/java/ghidra/app/plugin/core/functiongraph/FunctionGraphGroupVertices1Test.java +++ /dev/null @@ -1,1000 +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.app.plugin.core.functiongraph; - -import static org.junit.Assert.*; - -import java.awt.Color; -import java.awt.geom.Point2D; -import java.util.*; - -import org.apache.commons.collections4.IterableUtils; -import org.junit.*; - -import docking.ActionContext; -import docking.action.DockingActionIf; -import docking.theme.GThemeDefaults.Colors.Palette; -import edu.uci.ics.jung.graph.Graph; -import ghidra.app.plugin.core.clear.ClearPlugin; -import ghidra.app.plugin.core.codebrowser.CodeBrowserPlugin; -import ghidra.app.plugin.core.functiongraph.graph.FGEdge; -import ghidra.app.plugin.core.functiongraph.graph.FunctionGraph; -import ghidra.app.plugin.core.functiongraph.graph.vertex.FGVertex; -import ghidra.app.plugin.core.functiongraph.graph.vertex.GroupedFunctionGraphVertex; -import ghidra.app.plugin.core.functiongraph.mvc.*; -import ghidra.framework.plugintool.util.PluginException; -import ghidra.graph.viewer.options.RelayoutOption; -import ghidra.program.model.address.Address; -import ghidra.program.model.address.AddressSetView; -import util.CollectionUtils; - -public class FunctionGraphGroupVertices1Test extends AbstractFunctionGraphTest { - - @Override - @Before - public void setUp() throws Exception { - super.setUp(); - disableAnimation(); - } - - @Test - public void testGroupAndUngroupVertices() { - doTestGroupAndUngroupVertices(); - } - - @Test - public void testGroupAndUngroupWithAutomaticRelayoutOff() { - FGController controller = getFunctionGraphController(); - FunctionGraphOptions options = controller.getFunctionGraphOptions(); - setInstanceField("relayoutOption", options, RelayoutOption.NEVER); - - doTestGroupAndUngroupVertices(); - } - - @Test - public void testGroupingPersistence() throws Exception { - // - // Round-trip test to ensure that a grouped graph will be restored after re-opening a - // program. - // - - // - // Pick a function and group some nodes. - // - FGData graphData = graphFunction("01002cf5"); - FunctionGraph functionGraph = graphData.getFunctionGraph(); - Graph graph = functionGraph; - - Set ungroupedVertices = - selectVertices(functionGraph, "01002d2b" /* Another Local*/, "01002d1f" /* MyLocal */); - Set ungroupedEdges = getEdges(graph, ungroupedVertices); - - group(ungroupedVertices); - - // -1 because one one of the edges was between two of the vertices being grouped - int expectedGroupedEdgeCount = ungroupedEdges.size() - 1; - GroupedFunctionGraphVertex groupedVertex = validateNewGroupedVertexFromVertices( - functionGraph, ungroupedVertices, expectedGroupedEdgeCount); - Point2D location = getLocation(groupedVertex); - AddressSetView addresses = groupedVertex.getAddresses(); - Address minAddress = addresses.getMinAddress(); - Address maxAddress = addresses.getMaxAddress(); - - // Record the edges for later validation. Note: we have to keep the string form, as the - // toString() on the edges will call back to its vertices, which will later have been - // disposed. - Collection oringalGroupedEdges = new HashSet<>(graph.getEdges());// copy so they don't get cleared - List originalEdgeStrings = new ArrayList<>(oringalGroupedEdges.size()); - for (FGEdge edge : oringalGroupedEdges) { - originalEdgeStrings.add(edge.toString()); - } - - // debug - capture(getPrimaryGraphViewer(), "graph.grouping.before.reload"); - graphData = triggerPersistenceAndReload("01002cf5"); - - waitForAnimation();// the re-grouping may be using animation, which runs after the graph is loaded - functionGraph = graphData.getFunctionGraph(); - graph = functionGraph; - FGVertex vertex = functionGraph.getVertexForAddress(minAddress); - assertTrue(vertex instanceof GroupedFunctionGraphVertex); - assertEquals(maxAddress, vertex.getAddresses().getMaxAddress()); - - Point2D newLocation = getLocation(vertex); - - // TODO debug - this has failed; suspected timing issue - waitForCondition(() -> pointsAreSimilar(location, newLocation)); - - capture(getPrimaryGraphViewer(), "graph.grouping.after.reload"); - assertTrue( - "Vertex location not restored to default after performing a relayout " + - "original point: " + location + " - reloaded point: " + newLocation, - pointsAreSimilar(location, newLocation)); - - Collection newGroupedEdges = graph.getEdges(); - List newEdgeStrings = new ArrayList<>(newGroupedEdges.size()); - for (FGEdge edge : newGroupedEdges) { - newEdgeStrings.add(edge.toString()); - } - - assertSameEdges("Edges not correctly restored after persisting", originalEdgeStrings, - newEdgeStrings); - } - - /** - * Tests that the app will recognize the case where the entry point to a function is invalid, - * and generate the appropriate error message when trying to create a function graph. - * - * Step 1: Make sure the function graph window is closed. - * Step 2: Clear the entry point bytes - * Step 3: Open the function graph window to generate the graph again. - * Step 4: Check the error message. - */ - public void testInvalidFunctionEntryPoint() { - - // First thing we need to do is close the function graph window. It's opened on - // startup by default in this test suite but we want it closed until we clear the - // function code bytes. - this.getFunctionGraphController().getProvider().closeComponent(); - - // Set up some additional plugins we need. - try { - tool.addPlugin(CodeBrowserPlugin.class.getName()); - tool.addPlugin(ClearPlugin.class.getName()); - } - catch (PluginException e) { - e.printStackTrace(); - return; - } - FunctionGraphPlugin fgp = getPlugin(tool, FunctionGraphPlugin.class); - ClearPlugin cp = getPlugin(tool, ClearPlugin.class); - CodeBrowserPlugin cb = env.getPlugin(CodeBrowserPlugin.class); - - // Clear the entry point instruction. - DockingActionIf clearAction; - clearAction = getAction(cp, "Clear Code Bytes"); - cb.goToField(program.getAddressFactory().getAddress("01002cf5"), "Address", 0, 0); - final ActionContext context = cb.getProvider().getActionContext(null); - runSwing(() -> clearAction.actionPerformed(context)); - waitForBusyTool(tool); - - // Open the window; the tool will try to generate a new graph but should fail and generate - // an error message. - DockingActionIf openGraphAction; - openGraphAction = getAction(fgp, "Display Function Graph"); - runSwing(() -> openGraphAction.actionPerformed(context)); - waitForBusyTool(tool); - - // Assert that the graph has generated the correct error message and stored it in - // the function graph data. - FGController controller = getFunctionGraphController(); - FGData data = controller.getFunctionGraphData(); - assertTrue(data.getMessage().contains("No instruction at function entry point")); - } - - @Test - public void testGroupAndUngroup_WhenOneOfTheGroupIsAGroup() { - // - // This test seeks to ensure that you can group a selection of vertices when one of - // those vertices is itself a group. - // - - FGData graphData = graphFunction("01002cf5"); - FunctionGraph functionGraph = graphData.getFunctionGraph(); - Graph graph = functionGraph; - - Set ungroupedVertices = - selectVertices(functionGraph, "01002d2b" /* Another Local*/, "01002d1f" /* MyLocal */); - Set ungroupedEdges = getEdges(graph, ungroupedVertices); - - group(ungroupedVertices); - - // -1 because one one of the edges was between two of the vertices being grouped - int expectedGroupedEdgeCount = ungroupedEdges.size() - 1; - GroupedFunctionGraphVertex groupedVertex = validateNewGroupedVertexFromVertices( - functionGraph, ungroupedVertices, expectedGroupedEdgeCount); - assertVerticesRemoved(graph, ungroupedVertices); - assertEdgesRemoved(graph, ungroupedEdges); - - // - // Now group the group vertex with another vertex - // - Set secondUngroupedVertices = selectVertices(functionGraph, - "01002d0f" /* LAB_01002d0f */, "01002d1f" /* Grouped Vertex */); - Set secondUngroupedEdges = getEdges(graph, secondUngroupedVertices); - - group(secondUngroupedVertices); - - // 5 edges expected: - // -ungrouped vertex: 1 in, 1 out - // -grouped vertex : 1 in, 2 out - expectedGroupedEdgeCount = 5; - GroupedFunctionGraphVertex secondGroupedVertex = validateNewGroupedVertexFromVertices( - functionGraph, secondUngroupedVertices, expectedGroupedEdgeCount); - assertVerticesRemoved(graph, secondUngroupedVertices); - assertEdgesRemoved(graph, secondUngroupedEdges); - - // - // Ungrouping the first time should restore the previous grouped vertices, including the - // group vertex. - // - - ungroup(secondGroupedVertex); - - assertVertexRemoved(graph, secondGroupedVertex); - assertVerticesAdded(graph, secondUngroupedVertices); - assertEdgesAdded(functionGraph, secondUngroupedEdges); - assertSelected(secondUngroupedVertices); - - ungroup(groupedVertex); - - assertVertexRemoved(graph, groupedVertex); - assertVerticesAdded(graph, ungroupedVertices); - assertEdgesAdded(functionGraph, ungroupedEdges); - assertSelected(ungroupedVertices); - } - - @Test - public void testGroupingPersistence_WhenOneOfTheGroupIsAGroup() throws Exception { - // - // This test seeks to ensure that groups within groups are persisted and restored. - // - - FGData graphData = graphFunction("01002cf5"); - FunctionGraph functionGraph = graphData.getFunctionGraph(); - Graph graph = functionGraph; - - Set ungroupedVertices = - selectVertices(functionGraph, "01002d2b" /* Another Local*/, "01002d1f" /* MyLocal */); - Set ungroupedEdges = getEdges(graph, ungroupedVertices); - - //printEdges(ungroupedEdges); - group(ungroupedVertices); - - // -1 because one one of the edges was between two of the vertices being grouped - int expectedGroupedEdgeCount = ungroupedEdges.size() - 1; - GroupedFunctionGraphVertex innerGroupedVertex = validateNewGroupedVertexFromVertices( - functionGraph, ungroupedVertices, expectedGroupedEdgeCount); - assertVerticesRemoved(graph, ungroupedVertices); - assertEdgesRemoved(graph, ungroupedEdges); - - AddressSetView addresses = innerGroupedVertex.getAddresses(); - Address innerMinAddress = addresses.getMinAddress(); - Address innerMaxAddress = addresses.getMaxAddress(); - - // - // Now group the group vertex with another vertex - // - Set outerUngroupedVertices = selectVertices(functionGraph, - "01002d0f" /* LAB_01002d0f */, "01002d1f" /* Grouped Vertex */); - Set outerUngroupedEdges = getEdges(graph, outerUngroupedVertices); - - //printEdges(outerUngroupedEdges); - group(outerUngroupedVertices); - - // 5 edges expected: - // -ungrouped vertex: 1 in, 1 out - // -grouped vertex : 1 in, 2 out - expectedGroupedEdgeCount = 5; - GroupedFunctionGraphVertex outerGroupedVertex = validateNewGroupedVertexFromVertices( - functionGraph, outerUngroupedVertices, expectedGroupedEdgeCount); - assertVerticesRemoved(graph, outerUngroupedVertices); - assertEdgesRemoved(graph, outerUngroupedEdges); - - AddressSetView outerAddresses = outerGroupedVertex.getAddresses(); - Address secondMinAddress = outerAddresses.getMinAddress(); - Address secondMaxAddress = outerAddresses.getMaxAddress(); - - graphData = triggerPersistenceAndReload("01002cf5"); - - waitForAnimation();// the re-grouping may be using animation, which runs after the graph is loaded - functionGraph = graphData.getFunctionGraph(); - graph = functionGraph; - FGVertex vertex = functionGraph.getVertexForAddress(secondMinAddress); - assertTrue(vertex instanceof GroupedFunctionGraphVertex); - assertEquals(secondMaxAddress, vertex.getAddresses().getMaxAddress()); - outerGroupedVertex = (GroupedFunctionGraphVertex) vertex; - -// outerUngroupedVertices = -// selectVertices(functionGraph, "01002d0f" /* LAB_01002d0f */, "01002d1f" /* Grouped Vertex */); -// outerUngroupedEdges = getEdges(graph, outerUngroupedVertices); - - //printEdges(outerUngroupedEdges); - ungroup(outerGroupedVertex); - - vertex = functionGraph.getVertexForAddress(innerMinAddress); - assertTrue(vertex instanceof GroupedFunctionGraphVertex); - assertEquals(innerMaxAddress, vertex.getAddresses().getMaxAddress()); - innerGroupedVertex = (GroupedFunctionGraphVertex) vertex; - - //printEdges(outerUngroupedEdges); - assertEdgesAdded(functionGraph, outerUngroupedEdges); - - ungroup(innerGroupedVertex); - - assertEdgesAdded(functionGraph, ungroupedEdges); - } - - @Test - public void testRestoringWhenCodeBlocksHaveChanged_WillRegroup() { - int transactionID = -1; - try { - transactionID = program.startTransaction(testName.getMethodName()); - doTestRestoringWhenCodeBlocksHaveChanged_WillRegroup(); - } - finally { - program.endTransaction(transactionID, false); - } - } - - @Test - public void testSymbolAddedWhenGrouped_SymbolInsideOfGroupNode() { - int transactionID = -1; - try { - transactionID = program.startTransaction(testName.getMethodName()); - doTestSymbolAddedWhenGrouped_SymbolInsideOfGroupNode(); - } - finally { - program.endTransaction(transactionID, false); - } - } - - @Test - public void testUngroupAll() { - // - // Group some vertices and then group that vertex with some vertices to create a - // recursively/nested grouping. Also create a second top-level group. Make sure the - // ungroup all action will restore the original graph. - // - - FGData graphData = graphFunction("01002cf5"); - FunctionGraph functionGraph = graphData.getFunctionGraph(); - Graph graph = functionGraph; - Set originalVertices = new HashSet<>(graph.getVertices()); - - Set ungroupedVertices = - selectVertices(functionGraph, "01002d2b" /* Another Local*/, "01002d1f" /* MyLocal */); - group(ungroupedVertices); - - Set outerUngroupedVertices = selectVertices(functionGraph, - "01002d0f" /* LAB_01002d0f */, "01002d1f" /* Grouped Vertex */); - group(outerUngroupedVertices); - - Set secondUngroupedVertices = - selectVertices(functionGraph, "01002d11" /* LAB_01002d11*/, "01002d06" /* 01002d06 */); - group(secondUngroupedVertices); - - Assert.assertNotEquals(originalVertices.size(), graph.getVertices().size()); - - ungroupAll(); - - // don't use assertEQuals() with the different sets, as the sets may be of differing types - // that do not correctly compare as equal - Collection vertices = graph.getVertices(); - assertEquals(originalVertices.size(), vertices.size()); - for (FGVertex originalVertex : originalVertices) { - assertTrue("Original vertex not in ungrouped group: " + originalVertex, - vertices.contains(originalVertex)); - } - } - - @Test - public void testSetUserText_WithPersistence() { - FGData graphData = graphFunction("01002cf5"); - FunctionGraph functionGraph = graphData.getFunctionGraph(); - Graph graph = functionGraph; - - Set ungroupedVertices = - selectVertices(functionGraph, "01002d2b" /* Another Local*/, "01002d1f" /* MyLocal */); - Set ungroupedEdges = getEdges(graph, ungroupedVertices); - assertEquals("Did not grab all known edges for vertices", 4, ungroupedEdges.size()); - - String groupVertexText = "Test Text"; - group(ungroupedVertices, groupVertexText); - - assertVerticesRemoved(graph, ungroupedVertices); - assertEdgesRemoved(graph, ungroupedEdges); - - // -1 because one one of the edges was between two of the vertices being grouped - int expectedGroupedEdgeCount = ungroupedEdges.size() - 1; - GroupedFunctionGraphVertex groupedVertex = validateNewGroupedVertexFromVertices( - functionGraph, ungroupedVertices, expectedGroupedEdgeCount); - AddressSetView addresses = groupedVertex.getAddresses(); - Address minAddress = addresses.getMinAddress(); - Address maxAddress = addresses.getMaxAddress(); - - graphData = triggerPersistenceAndReload("01002cf5"); - - waitForAnimation();// the re-grouping may be using animation, which runs after the graph is loaded - functionGraph = graphData.getFunctionGraph(); - graph = functionGraph; - FGVertex vertex = functionGraph.getVertexForAddress(minAddress); - assertTrue(vertex instanceof GroupedFunctionGraphVertex); - assertEquals(maxAddress, vertex.getAddresses().getMaxAddress()); - - groupedVertex = (GroupedFunctionGraphVertex) vertex; - assertEquals("User-defined grouped vertex text was not restored after graph reload", - groupVertexText, groupedVertex.getUserText()); - } - - @Test - public void testGroupColoring_WithNoColorsInGroupedVertices() { - // - // The coloring algorithm: - // 1) If the grouped vertices are not colored, then use the default group color - // 2) If the grouped vertices are colored, but not all the same color, - // then use the default group color= - // 3) If all grouped vertices share the same color, then make the group that color - // - // This test is for 1) - - graphFunction("01002cf5"); - - // - // Group a node - // - FGVertex v1 = vertex("01002d06"); - FGVertex v2 = vertex("01002d0f"); - GroupedFunctionGraphVertex group = group("A", v1, v2); - - // - // Test the group node color - // - verifyDefaultColor(group); - - // - // Ungroup - // - ungroup(group); - - // - // Test the grouped vertices colors - // - verifyDefaultColor(v1, v2); - } - - @Test - public void testGroupColoring_WithMixedColorsInGroupedVertices() { - // - // The coloring algorithm: - // 1) If the grouped vertices are not colored, then use the default group color - // 2) If the grouped vertices are colored, but not all the same color, - // then use the default group color= - // 3) If all grouped vertices share the same color, then make the group that color - // - // This test is for 2) - - graphFunction("01002cf5"); - - // - // Group a node - // - FGVertex v1 = vertex("01002d06"); - FGVertex v2 = vertex("01002d0f"); - - // color just one of the vertices - Color newColor = Palette.RED; - color(v1, newColor); - - GroupedFunctionGraphVertex group = group("A", v1, v2); - - // - // Test the group node color - // - verifyDefaultColor(group); - - // - // Ungroup - // - ungroup(group); - - // - // Test the grouped vertices colors - // - verifyColor(v1, newColor); - verifyDefaultColor(v2); - } - - @Test - public void testGroupColoring_WithUniformColorsInGroupedVertices() { - // - // The coloring algorithm: - // 1) If the grouped vertices are not colored, then use the default group color - // 2) If the grouped vertices are colored, but not all the same color, - // then use the default group color= - // 3) If all grouped vertices share the same color, then make the group that color - // - // This test is for 3) - - graphFunction("01002cf5"); - - // - // Group a node - // - FGVertex v1 = vertex("01002d06"); - FGVertex v2 = vertex("01002d0f"); - - // color just one of the vertices - Color newColor = Palette.RED; - color(v1, newColor); - color(v2, newColor); - - GroupedFunctionGraphVertex group = group("A", v1, v2); - - // - // Test the group node color - // - verifyColor(group, newColor); - - // - // Ungroup - // - ungroup(group); - - // - // Test the grouped vertices colors - // - verifyColor(v1, newColor); - verifyColor(v2, newColor); - } - - @Test - public void testGroupColoring_WithNoColorsInGroupedVertices_ChangeWhileGroupedChangesInternalVertices() { - graphFunction("01002cf5"); - - // - // Group a node - // - FGVertex v1 = vertex("01002d06"); - FGVertex v2 = vertex("01002d0f"); - GroupedFunctionGraphVertex group = group("A", v1, v2); - - // - // Change the group color - // - Color newGroupColor = Palette.CYAN; - color(group, newGroupColor); - - // - // Ungroup - // - ungroup(group); - - // - // Test the grouped vertices colors - // - verifyColor(v1, newGroupColor); - verifyColor(v2, newGroupColor); - } - - @Test - public void testGroupColoring_WithMixedColorsInGroupedVertices_ChangeWhileGroupedChangesInternalVertices() { - - graphFunction("01002cf5"); - - // - // Group a node - // - FGVertex v1 = vertex("01002d06"); - FGVertex v2 = vertex("01002d0f"); - - // color just one of the vertices - Color newColor = Palette.RED; - color(v1, newColor); - - GroupedFunctionGraphVertex group = group("A", v1, v2); - - // - // Change the group color - // - Color newGroupColor = Palette.CYAN; - color(group, newGroupColor); - - // - // Ungroup - // - ungroup(group); - - // - // Test the grouped vertices colors - // - verifyColor(v1, newGroupColor); - verifyColor(v2, newGroupColor); - } - - @Test - public void testGroupColoring_WithUniformColorsInGroupedVertices_ChangeWhileGroupedChangesInternalVertices() { - graphFunction("01002cf5"); - - // - // Group a node - // - FGVertex v1 = vertex("01002d06"); - FGVertex v2 = vertex("01002d0f"); - - // color just one of the vertices - Color newColor = Palette.RED; - color(v1, newColor); - color(v2, newColor); - - GroupedFunctionGraphVertex group = group("A", v1, v2); - - // - // Change the group color - // - Color newGroupColor = Palette.CYAN; - color(group, newGroupColor); - - // - // Ungroup - // - ungroup(group); - - // - // Test the grouped vertices colors - // - verifyColor(v1, newGroupColor); - verifyColor(v2, newGroupColor); - } - - @Test - public void testGroupColorChangesGroupedVertexColors_AfterPeristence() { - graphFunction("01002cf5"); - - // - // Group a node - // - FGVertex v1 = vertex("01002d06"); - FGVertex v2 = vertex("01002d0f"); - GroupedFunctionGraphVertex group = group("A", v1, v2); - - // - // Change the group color - // - Color newGroupColor = Palette.CYAN; - color(group, newGroupColor); - - // - // Trigger persistence - // - Address groupAddress = group.getVertexAddress(); - FGData graphData = triggerPersistenceAndReload("01002cf5"); - - // - // Retrieve the group and make sure its color is restored - // - group = getGroupVertex(graphData.getFunctionGraph(), groupAddress); - verifyColor(group, newGroupColor); - - // - // Ungroup - // - ungroup(group); - - // - // Test the grouped vertices colors - // - v1 = vertex("01002d06"); - v2 = vertex("01002d0f"); - verifyColor(v1, newGroupColor); - verifyColor(v2, newGroupColor); - } - - @Test - public void testNoGroupColorChange_GroupedVertexColorsStillDefault_AfterPeristence() { - graphFunction("01002cf5"); - - // - // Group a node - // - FGVertex v1 = vertex("01002d06"); - FGVertex v2 = vertex("01002d0f"); - GroupedFunctionGraphVertex group = group("A", v1, v2); - - // - // Trigger persistence - // - Address groupAddress = group.getVertexAddress(); - FGData graphData = triggerPersistenceAndReload("01002cf5"); - - // - // Retrieve the group and make sure its color is restored - // - group = getGroupVertex(graphData.getFunctionGraph(), groupAddress); - verifyDefaultColor(group); - - // - // Ungroup - // - ungroup(group); - - // - // Test the grouped vertices colors - // - v1 = vertex("01002d06"); - v2 = vertex("01002d0f"); - verifyDefaultColor(v1, v2); - } - - @Test - public void testNoGroupColorChange_GroupedVertexColorsNonDefault_AfterPeristence() { - graphFunction("01002cf5"); - - FGVertex v1 = vertex("01002d06"); - FGVertex v2 = vertex("01002d0f"); - - // - // Color just one of the vertices - // - Color newColor = Palette.RED; - color(v1, newColor); - - // - // Group a node - // - GroupedFunctionGraphVertex group = group("A", v1, v2); - - // - // Trigger persistence - // - Address groupAddress = group.getVertexAddress(); - FGData graphData = triggerPersistenceAndReload("01002cf5"); - - // - // Retrieve the group and make sure its color is restored - // - group = getGroupVertex(graphData.getFunctionGraph(), groupAddress); - verifyDefaultColor(group); - - // - // Ungroup - // - ungroup(group); - - // - // Test the grouped vertices colors - // - v1 = vertex("01002d06"); - v2 = vertex("01002d0f"); - verifyColor(v1, newColor); - verifyDefaultColor(v2); - } - - @Test - public void testNoGroupColorChange_GroupedVertexColorsNonDefault_AfterReset() { - graphFunction("01002cf5"); - - FGVertex v1 = vertex("01002d06"); - FGVertex v2 = vertex("01002d0f"); - - // - // Color just one of the vertices - // - Color newColor = Palette.RED; - color(v1, newColor); - - // - // Group a node - // - GroupedFunctionGraphVertex group = group("A", v1, v2); - - // - // Trigger reset - // - Address groupAddress = group.getVertexAddress(); - FGData graphData = reset(); - - // - // Make sure the group is gone - // - FGVertex vertex = graphData.getFunctionGraph().getVertexForAddress(groupAddress); - assertFalse(vertex instanceof GroupedFunctionGraphVertex);// the group has been removed - - // - // Test the grouped vertices colors - // - v1 = vertex("01002d06"); - v2 = vertex("01002d0f"); - verifyColor(v1, newColor); - verifyDefaultColor(v2); - } - - @Test - public void testEdgeDefaultAlphaPersistsAfterGrouping() { - - graphFunction("01002cf5"); - - FGVertex v1 = vertex("01002cf5"); - FGVertex v2 = vertex("01002d0f"); - - FunctionGraph graph = getFunctionGraph(); - Iterable edges = graph.getEdges(v1, v2); - assertEquals(1, IterableUtils.size(edges)); - FGEdge edge = CollectionUtils.any(edges); - - Double alpha = edge.getAlpha(); - assertTrue(alpha < 1.0); // this is the default flow - - GroupedFunctionGraphVertex group = group("A", v1, v2); - ungroup(group); - - edges = graph.getEdges(v1, v2); - assertEquals(1, IterableUtils.size(edges)); - edge = CollectionUtils.any(edges); - - Double alphAfterGroup = edge.getAlpha(); - assertEquals(alpha, alphAfterGroup); - } - -//================================================================================================== -// Private Methods -//================================================================================================== - - // @formatter:off - @Override - protected void doTestGroupAndUngroupVertices() { - FGData graphData = graphFunction("01002cf5"); - FunctionGraph functionGraph = graphData.getFunctionGraph(); - Graph graph = functionGraph; - - Set ungroupedVertices = selectVertices( functionGraph, - "01002d2b" /* Another Local*/, - "01002d1f" /* MyLocal */); - Set ungroupedEdges = getEdges(graph, ungroupedVertices); - assertEquals("Did not grab all known edges for vertices", 4, ungroupedEdges.size()); - - group(ungroupedVertices); - - assertVerticesRemoved(graph, ungroupedVertices); - assertEdgesRemoved(graph, ungroupedEdges); - - // -1 because one one of the edges was between two of the vertices being grouped - int expectedGroupedEdgeCount = ungroupedEdges.size() - 1; - GroupedFunctionGraphVertex groupedVertex = - validateNewGroupedVertexFromVertices(functionGraph, ungroupedVertices, - expectedGroupedEdgeCount); - - ungroup(groupedVertex); - - assertVertexRemoved(graph, groupedVertex); - assertVerticesAdded(graph, ungroupedVertices); - assertEdgesAdded(functionGraph, ungroupedEdges); - assertSelected(ungroupedVertices); - - } - - @Override - protected void doTestRestoringWhenCodeBlocksHaveChanged_WillRegroup() { - // - // Tests the behavior of how group vertices are restored when one or more of the vertices - // inside of the grouped vertex is no longer available when the graph attempts to restore - // the group vertex user settings (i.e., when restarting Ghidra, the previously grouped - // vertices should reappear). - // - // In this test, we will be mutating a group of 3 nodes such - // that one of the nodes has been split into two. This leaves 2 vertices to - // be found by the regrouping algorithm. Furthermore, the regrouping *will* still - // take place, as at least two vertices cannot be found. - // - - // - // Pick a function and group some nodes. - // - FGData graphData = graphFunction("01002cf5"); - FunctionGraph functionGraph = graphData.getFunctionGraph(); - - Set ungroupedVertices = selectVertices(functionGraph, - "01002d11" /* LAB_01002d11 */, "01002cf5" /* ghidra */, "01002d1f" /* MyLocal */); - - group(ungroupedVertices); - - // 5 edges expected: - // -01002cf5: 2 out - // -01002d11: 2 in, (1 out that was removed) - // -01002d1f: 2 out (1 in that was removed) - int expectedGroupedEdgeCount = 6; - GroupedFunctionGraphVertex groupedVertex = validateNewGroupedVertexFromVertices( - functionGraph, ungroupedVertices, expectedGroupedEdgeCount); - - AddressSetView addresses = groupedVertex.getAddresses(); - Address minAddress = addresses.getMinAddress(); - Address maxAddress = addresses.getMaxAddress(); - - // - // Ideally, we would like to save, close and re-open the program so that we can get - // a round-trip saving and reloading. However, in the test environment, we cannot save - // our programs. So, we will instead just navigate away from the current function, clear - // the cache (to make sure that we read the settings again), and then verify that the - // data saved in the program has been used to re-group. - // - graphFunction("0100415a"); - clearCache(); - - // - // Add a label to trigger a code block change - // - Address labelAddress = createLabel("01002d18");// in the middle of the LAB_01002d11 code block - - // - // Relaunch the graph, which will use the above persisted group settings... - // - graphData = graphFunction("01002cf5"); - waitForAnimation();// the re-grouping may be using animation, which runs after the graph is loaded - functionGraph = graphData.getFunctionGraph(); - FGVertex expectedGroupVertex = functionGraph.getVertexForAddress(minAddress); - assertTrue(expectedGroupVertex instanceof GroupedFunctionGraphVertex); - assertEquals(maxAddress, expectedGroupVertex.getAddresses().getMaxAddress()); - - // ...we expect that the two original grouped vertices have again been grouped... - FGVertex splitVertex = - functionGraph.getVertexForAddress(getAddress("01002d11") /* LAB_01002d11 */); - assertTrue("The split vertex should not have been regrouped", - !(splitVertex instanceof GroupedFunctionGraphVertex)); - - FGVertex unchangedVertex = - functionGraph.getVertexForAddress(getAddress("01002cf5") /* ghidra */); - assertTrue("An unchanged vertex should have been regrouped: " + unchangedVertex, - (unchangedVertex instanceof GroupedFunctionGraphVertex)); - - unchangedVertex = functionGraph.getVertexForAddress(getAddress("01002d1f") /* MyLocal */); - assertTrue("An unchanged vertex should have been regrouped: " + unchangedVertex, - (unchangedVertex instanceof GroupedFunctionGraphVertex)); - - // ...but the newly created code block has not - FGVertex newlyCreatedVertex = functionGraph.getVertexForAddress(labelAddress); - assertNotNull(newlyCreatedVertex); - } - - @Override - protected void doTestSymbolAddedWhenGrouped_SymbolInsideOfGroupNode() { - // - // By default, if the FunctionGraph detects a symbol addition to one of the code blocks - // in the graph, then it will split the affected vertex (tested elsewhere). - // However, if the affected vertex is grouped, then the FG will not split the node, but - // should still show the 'stale' indicator. - // - - // - // Pick a function and group some nodes. - // - FGData graphData = graphFunction("01002cf5"); - FunctionGraph functionGraph = graphData.getFunctionGraph(); - - Set ungroupedVertices = - selectVertices(functionGraph, "01002d11" /* LAB_01002d11 */, "01002cf5" /* ghidra */); - - group(ungroupedVertices); - - // 5 edges expected: - // -01002cf5: 2 out - // -01002cf5: 2 in, 1 out - int expectedGroupedEdgeCount = 5; - GroupedFunctionGraphVertex groupedVertex = validateNewGroupedVertexFromVertices( - functionGraph, ungroupedVertices, expectedGroupedEdgeCount); - - // - // Add a label to trigger a code block change - // - Address labelAddress = createLabel("01002d18");// in the middle of the LAB_01002d11 code block - - // - // Make sure the newly created code block does not have a corresponding vertex - // - FGVertex exisingVertex = functionGraph.getVertexForAddress(labelAddress); - assertEquals("Grouped vertex does not contain the address of the newly created label", - groupedVertex, exisingVertex); - } -} diff --git a/Ghidra/Features/FunctionGraph/src/test/java/ghidra/app/plugin/core/functiongraph/FunctionGraphPlugin1Test.java b/Ghidra/Features/FunctionGraph/src/test/java/ghidra/app/plugin/core/functiongraph/FunctionGraphPlugin1Test.java deleted file mode 100644 index 8337052dc8..0000000000 --- a/Ghidra/Features/FunctionGraph/src/test/java/ghidra/app/plugin/core/functiongraph/FunctionGraphPlugin1Test.java +++ /dev/null @@ -1,980 +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.app.plugin.core.functiongraph; - -import static ghidra.graph.viewer.GraphViewerUtils.*; -import static org.junit.Assert.*; - -import java.awt.*; -import java.awt.datatransfer.Clipboard; -import java.awt.datatransfer.Transferable; -import java.awt.geom.Point2D; -import java.util.*; -import java.util.List; -import java.util.stream.Collectors; - -import org.junit.*; - -import docking.ActionContext; -import docking.ComponentProvider; -import docking.action.DockingAction; -import docking.dnd.GClipboard; -import docking.theme.GThemeDefaults.Colors.Palette; -import edu.uci.ics.jung.algorithms.layout.Layout; -import edu.uci.ics.jung.visualization.VisualizationModel; -import edu.uci.ics.jung.visualization.VisualizationViewer; -import edu.uci.ics.jung.visualization.util.Caching; -import generic.test.TestUtils; -import ghidra.app.cmd.label.AddLabelCmd; -import ghidra.app.events.ProgramSelectionPluginEvent; -import ghidra.app.nav.LocationMemento; -import ghidra.app.nav.Navigatable; -import ghidra.app.plugin.core.colorizer.ColorizingPlugin; -import ghidra.app.plugin.core.colorizer.ColorizingService; -import ghidra.app.plugin.core.functiongraph.graph.*; -import ghidra.app.plugin.core.functiongraph.graph.vertex.FGVertex; -import ghidra.app.plugin.core.functiongraph.mvc.*; -import ghidra.app.plugin.core.navigation.GoToAddressLabelPlugin; -import ghidra.app.plugin.core.navigation.NextPrevAddressPlugin; -import ghidra.app.services.*; -import ghidra.program.model.address.Address; -import ghidra.program.model.address.AddressSetView; -import ghidra.program.model.block.*; -import ghidra.program.model.listing.Function; -import ghidra.program.model.listing.FunctionManager; -import ghidra.program.model.symbol.SourceType; -import ghidra.program.util.ProgramLocation; -import ghidra.program.util.ProgramSelection; -import ghidra.util.task.TaskMonitor; - -public class FunctionGraphPlugin1Test extends AbstractFunctionGraphTest { - - public FunctionGraphPlugin1Test() { - super(); - } - - @Override - @Before - public void setUp() throws Exception { - super.setUp(); - - tool.addPlugin(GoToAddressLabelPlugin.class.getName()); - tool.addPlugin(NextPrevAddressPlugin.class.getName()); - - waitForGraphToLoad(); - } - -// public void testCodeBlockSpeed() throws CancelledException { -//// ProgramDB cppProgram = env.getProgram("msvidctl.dll_MFCAnalysis"); -// ProgramDB cppProgram = env.getProgram("winword.exe"); -// -// FunctionManager functionManager = cppProgram.getFunctionManager(); -//// Function function = functionManager.getFunctionAt(getAddress("5a1f903e")); -// Function function = functionManager.getFunctionAt(getAddress("3036d629")); -// -// long startTime = System.nanoTime(); -// BasicBlockModel blockModel = new BasicBlockModel(cppProgram, false); -// CodeBlockIterator iterator = -// blockModel.getCodeBlocksContaining(function.getBody(), TaskMonitorAdapter.DUMMY_MONITOR); -// while (iterator.hasNext()) { -// iterator.next(); -// } -// long endTime = System.nanoTime(); -// double totalTime = (endTime - startTime) / 1000000000d; -// System.err.println("total time: " + totalTime); -// } - - @Test - public void testLocationChanged() { - // get the graph contents - FGData graphData = getFunctionGraphData(); - assertNotNull(graphData); - assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults()); - ProgramLocation location = getLocationForAddressString(startAddressString); - assertTrue(graphData.containsLocation(location)); - FunctionGraph functionGraph = graphData.getFunctionGraph(); - - // locate vertex with cursor - FGVertex focusedVertex = getFocusVertex(functionGraph); - assertNotNull("We did not start with a focused vertex", focusedVertex); - - // change the location - String newLocationString = "01004192"; - goToAddress(newLocationString); - - // locate vertex with cursor - graphData = getFunctionGraphData(); - assertNotNull(graphData); - assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults()); - ProgramLocation newLocation = getLocationForAddressString(newLocationString); - assertTrue(graphData.containsLocation(newLocation)); - - functionGraph = graphData.getFunctionGraph(); - FGVertex newFocusedVertex = functionGraph.getFocusedVertex(); - assertTrue(newFocusedVertex.containsProgramLocation(newLocation)); - - // make sure the two vertices are not the same - assertTrue("Changing locations in the code browser did not move the cursor location to " + - "a new graph vertex", !focusedVertex.equals(newFocusedVertex)); - } - - @Test - public void testProgramSelectionAcrossVerticesFromCodeBrowser() { - FGData graphData = getFunctionGraphData();// make sure the graph gets loaded - assertNotNull(graphData); - assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults()); - - ProgramSelection ps = makeMultiVertexSelectionInCodeBrowser(); - - // this address is in a different vertex than the start address - ProgramLocation location = getLocationForAddressString("0x01004192"); - assertTrue(graphData.containsLocation(location)); - FunctionGraph functionGraph = graphData.getFunctionGraph(); - FGVertex startVertex = functionGraph.getVertexForAddress(location.getAddress()); - - // locate vertex with cursor - assertNotNull("We did not start with a focused vertex", startVertex); - assertTrue(startVertex.containsProgramLocation(location)); - - // make a selection starting at the current location - ProgramSelection firstSelection = startVertex.getProgramSelection(); - assertTrue("A selection too big for one vertex has fit into a start vertex", - !ps.equals(firstSelection)); - - Address address = getAddress("0x01004196"); - FGVertex secondVertex = functionGraph.getVertexForAddress(address); - ProgramSelection secondSelection = secondVertex.getProgramSelection(); - assertTrue(!secondSelection.isEmpty()); - - assertTrue(ps.getMinAddress().equals(firstSelection.getMinAddress())); - assertTrue(ps.getMaxAddress().equals(secondSelection.getMaxAddress())); - } - - @Test - public void testGraphWithCloseAndReopenProgram_ForSCR_7813() { - // - // This test is meant to ensure that graph contents are properly disposed and that no - // exceptions happen while switching programs. - // - FGData graphData = getFunctionGraphData(); - assertNotNull(graphData); - assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults()); - - ProgramManager pm = tool.getService(ProgramManager.class); - pm.closeProgram(program, true); - - // should have empty data after closing the program - graphData = getFunctionGraphData(); - assertNotNull(graphData); - assertTrue("Graph data should be empty after closing the program", !graphData.hasResults()); - - pm.openProgram(program.getDomainFile()); - program.flushEvents(); - - // we should have some sort of non-null data--either real or empty - graphData = getFunctionGraphData(); - assertNotNull(graphData); - - goToAddress(startAddressString); - - // verify we can still graph data - graphData = getFunctionGraphData(); - assertNotNull(graphData); - assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults()); - } - -// note: unreliable--garbage collection works differently across platforms (sad face) -// public void testClearCacheAndMemoryLeak() { -// // -// // Test that when we clear the cache of a graph the vertices of that graph will be -// // garbage collected -// // -// WeakSet weakSet = -// WeakDataStructureFactory.createSingleThreadAccessWeakSet(); -// -// FunctionGraphData graphData = getFunctionGraphData(); -// assertNotNull(graphData); -// assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults()); -// -// FunctionGraph graph = graphData.getFunctionGraph(); -// FunctionGraphVertex rootVertex = graph.getRootVertex(); -// assertNotNull(rootVertex); -// -// weakSet.add(rootVertex); -// assertTrue(weakSet.iterator().hasNext()); -// -// // move to a new function so that we the FG will not be holding a reference to the -// // originally graphed function -// String address = "01002239"; -// goToAddress(address); -// -// FunctionGraphData newGraphData = getFunctionGraphData(); -// assertNotNull(newGraphData); -// assertTrue("Unexpectedly received an empty FunctionGraphData", newGraphData.hasResults()); -// -// assertTrue( -// "Function Graph did not graph a new function as expected at address: " + address, -// !graphData.equals(newGraphData)); -// -// triggerGraphDisposal(graphData); -// -// rootVertex = null; -// graph = null; -// graphData = null; -// -// // let (force) the Garbage Collector run -// System.gc(); -// sleep(100); -// System.gc(); -// sleep(100); -// System.gc(); -// sleep(100); -// -// boolean isNotCollected = weakSet.iterator().hasNext(); -// assertFalse(isNotCollected); -// } -// -// private void triggerGraphDisposal(FunctionGraphData dataToDispose) { -// Object controller = getInstanceField("controller", graphProvider); -// LRUMap cache = (LRUMap) getInstanceField("graphCache", controller); -// cache.clear(); -// dataToDispose.dispose(); -// } - - @Test - public void testSaveVertexPositions() { - // - // Test that we can move a node, load a new graph, reload the original graph and have - // the moved node return to the moved position - // - - FGData originalGraphData = getFunctionGraphData(); - assertNotNull(originalGraphData); - assertTrue("Unexpectedly received an empty FunctionGraphData", - originalGraphData.hasResults()); - - FunctionGraph graph = originalGraphData.getFunctionGraph(); - final FGVertex rootVertex = graph.getRootVertex(); - assertNotNull(rootVertex); - - Point2D originalPoint = rootVertex.getLocation(); - - double dx = originalPoint.getX() + 100; - double dy = originalPoint.getY() + 100; - - Point2D newPoint = new Point2D.Double(dx, dy); - final Layout primaryLayout = getPrimaryLayout(); - final Point2D finalNewPoint = newPoint; - runSwing(() -> primaryLayout.setLocation(rootVertex, finalNewPoint)); - - // we have to wait for the paint to take place, as the rendering will change the vertex - // locations - FGPrimaryViewer primaryGraphViewer = getPrimaryGraphViewer(); - primaryGraphViewer.repaint(); - waitForSwing(); - - // now that we have changed the data, load a new graph and then come back to the start - // graph so that we can see that the settings have been re-applied - String address = "01002239"; - goToAddress(address); - - FGData newGraphData = getFunctionGraphData(); - assertNotNull(newGraphData); - assertTrue("Unexpectedly received an empty FunctionGraphData", newGraphData.hasResults()); - assertTrue(!newGraphData.equals(originalGraphData)); - - // ...now go back and check the position - goToAddress(startAddressString); - - newGraphData = getFunctionGraphData(); - assertNotNull(newGraphData); - assertTrue("Unexpectedly received an empty FunctionGraphData", newGraphData.hasResults()); - - graph = newGraphData.getFunctionGraph(); - FGVertex newRootVertex = graph.getRootVertex(); - assertNotNull(newRootVertex); - - waitForSwing(); - - // Note: we can't test for exact equality based upon the location we set, as the values - // are updated based upon other factors, like screen location and size. We want to make - // sure that the value is not the default. - Point2D reloadedPoint = newRootVertex.getLocation(); - assertTrue("Vertex location not restored after regraphing a function", - !originalPoint.equals(reloadedPoint)); - } - - @Test - public void testRelayout() throws Exception { - doTestRelayout(false); - } - - @Test - public void testReload() throws Exception { - doTestRelayout(true); - } - - @Test - public void testLabelChangeAtVertexEntryUpdatesTitle() { - int txID = -1; - try { - txID = program.startTransaction("Test: " + testName.getMethodName()); - doTestLabelChangeAtVertexEntryUpdatesTitle(); - } - finally { - program.endTransaction(txID, false); - } - } - - @Test - public void testChangeFormat() throws Exception { - // - // Test that we can change the view's format. As part of this test verify: - // -That each graph gets the view changes - // -Test reset format - // -That view changes are persisted - // - FGController primaryController = getFunctionGraphController(); - waitForBusyRunManager(primaryController); - - FGData functionGraphData = getFunctionGraphData(); - FunctionGraph functionGraph = functionGraphData.getFunctionGraph(); - - // Be sure to pick a vertex that will get bigger when a new field is added. The - // root vertex is already so wide (due to the function signature), that adding a small - // field does not change its width. - FGVertex vertex = functionGraph.getVertexForAddress(getAddress("1004178")); - Rectangle originalBounds = vertex.getBounds(); - - // Also, be sure that we are not on the function signature field, as that does not have - // the 'Bytes' field. - goTo("1004179"); - - addBytesFormatFieldFactory(); - - // - // Verify the vertex size has change (due to the format getting larger) - // - FGPrimaryViewer viewer = getPrimaryGraphViewer(); - viewer.repaint(); - waitForSwing(); - - Rectangle updatedBounds = vertex.getBounds(); - assertTrue("bounds not updated - was: " + originalBounds + "; is now: " + updatedBounds, - originalBounds.width < updatedBounds.width); - - performResetFormatAction(); - - viewer.repaint(); - waitForSwing(); - - Rectangle newNewBounds = vertex.getBounds(); - assertTrue(updatedBounds.width > newNewBounds.width); - } - - @Test - public void testCopyKeyBinding() throws Exception { - // - // Make a program selection and test that executing the copy keybinding will copy the - // selection (across vertices). - // - - // - // Initialize the clipboard with known data - // - Clipboard systemClipboard = GClipboard.getSystemClipboard(); - systemClipboard.setContents(DUMMY_TRANSFERABLE, null); - waitForSwing(); - - // - // Verify our initial state - FGData graphData = getFunctionGraphData(); - assertNotNull(graphData); - assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults()); - ProgramLocation location = getLocationForAddressString(startAddressString); - assertTrue(graphData.containsLocation(location)); - FunctionGraph functionGraph = graphData.getFunctionGraph(); - - // locate vertex with cursor - FGVertex focusedVertex = getFocusVertex(functionGraph); - assertNotNull("We did not start with a focused vertex", focusedVertex); - - // - // Create a selection that we will copy from the executing the action - // - AddressSetView addresses = focusedVertex.getAddresses(); - Address address = addresses.getMinAddress(); - ProgramSelection selection = - new ProgramSelection(program.getAddressFactory(), address, address.add(8)); - tool.firePluginEvent(new ProgramSelectionPluginEvent("Test", selection, program)); - - // - // Validate and execute the action - // - DockingAction copyAction = getCopyAction(); - FGController controller = getFunctionGraphController(); - ComponentProvider provider = controller.getProvider(); - assertTrue(copyAction.isEnabledForContext(provider.getActionContext(null))); - - performAction(copyAction, provider, false); - - waitForTasks(); - - Transferable contents = systemClipboard.getContents(systemClipboard); - assertNotNull(contents); - assertTrue("Contents not copied into system clipboard", (contents != DUMMY_TRANSFERABLE)); - } - - @Test - public void testCopyAction() { - // - // Put the cursor in a vertex on a field with text and make sure that the copy action - // is enabled. - // - - // - // Initialize the clipboard with known data - // - Clipboard systemClipboard = GClipboard.getSystemClipboard(); - systemClipboard.setContents(DUMMY_TRANSFERABLE, null); - waitForSwing(); - - // - // Verify our initial state - FGData graphData = getFunctionGraphData(); - assertNotNull(graphData); - assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults()); - ProgramLocation location = getLocationForAddressString(startAddressString); - assertTrue(graphData.containsLocation(location)); - FunctionGraph functionGraph = graphData.getFunctionGraph(); - - // locate vertex with cursor - FGVertex focusedVertex = getFocusVertex(functionGraph); - assertNotNull("We did not start with a focused vertex", focusedVertex); - - // - // Put the cursor on a copyable thing - // - codeBrowser.goToField(getAddress("0x01004196"), "Mnemonic", 0, 0, 2, true); - waitForSwing(); - - // sanity check - DockingAction copyAction = getCopyAction(); - assertClipboardServiceAddress(copyAction, "0x01004196"); - - // - // Validate and execute the action - // - FGController controller = getFunctionGraphController(); - ComponentProvider provider = controller.getProvider(); - ActionContext actionContext = provider.getActionContext(null); - boolean isEnabled = copyAction.isEnabledForContext(actionContext); - debugAction(copyAction, actionContext); - assertTrue(isEnabled); - performAction(copyAction, actionContext, true); - - Transferable contents = systemClipboard.getContents(systemClipboard); - assertNotNull(contents); - assertTrue("Contents not copied into system clipboard", (contents != DUMMY_TRANSFERABLE)); - } - - @Test - public void testSatelliteViewIsResizedToFit() { - // - // Test that the satellite view zoomed to fit completely in the window if the window - // is resized or the graph is made bigger via dragging a vertex. - // - showSatellite();// make sure it is on - - FGController controller = getFunctionGraphController(); - waitForBusyRunManager(controller); - FGView view = controller.getView(); - VisualizationViewer satelliteViewer = view.getSatelliteViewer(); - Double originalGraphScale = getGraphScale(satelliteViewer); - - // - // window size change test - // - final Window window = windowForComponent(graphProvider.getComponent()); - final Dimension originalSize = window.getSize(); - final Dimension newSize = new Dimension(originalSize.width >> 1, originalSize.height >> 1); - runSwing(() -> window.setSize(newSize)); - waitForSwing(); - waitForBusyGraph(); - - Double newGraphScale = getGraphScale(satelliteViewer); - Assert.assertNotEquals("The graph's scale did not change after resizing the window", - originalGraphScale, newGraphScale); - - runSwing(() -> window.setSize(originalSize)); - waitForSwing(); - waitForBusyGraph(); - - newGraphScale = getGraphScale(satelliteViewer); - assertEquals(originalGraphScale, newGraphScale); - - // - // graph size change test - // - FGData functionGraphData = getFunctionGraphData(); - FunctionGraph functionGraph = functionGraphData.getFunctionGraph(); - VisualizationViewer primaryGraphViewer = view.getPrimaryGraphViewer(); - VisualizationModel model = primaryGraphViewer.getModel(); - final Layout graphLayout = model.getGraphLayout(); - final FGVertex vertex = functionGraph.getRootVertex(); - final Point2D startPoint = graphLayout.apply(vertex); - - final Point2D newPoint = new Point2D.Double(startPoint.getX() + 2000, startPoint.getY()); - runSwing(() -> { - Caching cachingLayout = (Caching) graphLayout; - cachingLayout.clear(); - graphLayout.setLocation(vertex, newPoint); - }); - waitForSwing(); - - Double scaleAfterDragging = getGraphScale(satelliteViewer); - Assert.assertNotEquals(newGraphScale, scaleAfterDragging); - - // put the vertex back and make sure the scale is reverted - runSwing(() -> { - Caching cachingLayout = (Caching) graphLayout; - cachingLayout.clear(); - graphLayout.setLocation(vertex, startPoint); - }); - waitForSwing(); - - scaleAfterDragging = getGraphScale(satelliteViewer); - assertEquals(newGraphScale, scaleAfterDragging); - } - - public void TODO_testPersistence() { - // - // This wants to test that graph perspective info is saved between Ghidra sessions. In - // other words, is my graph location and zoom level the same between Ghidra runs. We - // also want to test that colors and vertex locations are persisted between sessions. - // - // - - // Note: This is probably too hard to worry about. To test this, we have to setup a - // Ghidra environment with varied values. Then, close down Ghidra, relaunch Ghidra, - // and test the changed values for sameness - } - - public void TODO_testEdgeHover() { - // hover should show information about the connected nodes in a tooltip? - - // should hover highlight the same edge in the satellite? - - // - // Note: these are GUI intensive tests--low reward/benefit ratios - // - } - - @Test - public void testGraphNodesCreated() throws Exception { - FGData graphData = getFunctionGraphData(); - assertNotNull(graphData); - assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults()); - FunctionGraph functionGraph = graphData.getFunctionGraph(); - Collection vertices = functionGraph.getVertices(); - - BlockModelService blockService = tool.getService(BlockModelService.class); - CodeBlockModel blockModel = blockService.getActiveBlockModel(program); - FunctionManager functionManager = program.getFunctionManager(); - Function function = functionManager.getFunctionContaining(getAddress(startAddressString)); - CodeBlockIterator iterator = - blockModel.getCodeBlocksContaining(function.getBody(), TaskMonitor.DUMMY); - - // we should have one vertex for each code block - Set
vertexAddresses = new HashSet<>(); - for (FGVertex vertex : vertices) { - AddressSetView addresses = vertex.getAddresses(); - vertexAddresses.add(addresses.getMinAddress()); - } - - for (; iterator.hasNext();) { - CodeBlock codeBlock = iterator.next(); - assertTrue(vertexAddresses.contains(codeBlock.getMinAddress())); - } - } - - @Test - public void testClearColorAction() throws Exception { - tool.addPlugin(ColorizingPlugin.class.getName()); - - FGVertex focusedVertex = getFocusedVertex(); - ColorizingService colorizingService = tool.getService(ColorizingService.class); - Color appliedBackgroundColor = - colorizingService.getBackgroundColor(focusedVertex.getVertexAddress()); - - Color testColor = Palette.RED; - assertTrue("Unexpected start color--must change the test!", - !testColor.equals(appliedBackgroundColor)); - - chooseColor(focusedVertex, testColor); - - Color newVertexBackgroundColor = focusedVertex.getUserDefinedColor(); - assertEquals("Background color not set", testColor, newVertexBackgroundColor); - - DockingAction clearColorAction = getClearColorAction(focusedVertex); - performAction(clearColorAction, graphProvider, true); - - Color userDefinedColor = focusedVertex.getUserDefinedColor(); - assertNull(userDefinedColor); - - Color serviceBackgroundColor = - colorizingService.getBackgroundColor(focusedVertex.getVertexAddress()); - assertNull("Clear action did not clear the service's applied color", - serviceBackgroundColor); - } - - // test that navigating a vertex updates the code browser's location - @Test - public void testNavigationFromVertexToCodeBrowser() { - - // - // This test covers navigation, which relies on the provider being focused to work - // - setProviderAlwaysFocused(); - - FGData graphData = getFunctionGraphData(); - assertNotNull(graphData); - assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults()); - ProgramLocation location = getLocationForAddressString(startAddressString); - assertTrue(graphData.containsLocation(location)); - FunctionGraph functionGraph = graphData.getFunctionGraph(); - - // locate vertex with cursor - FGVertex focusedVertex = getFocusVertex(functionGraph); - assertNotNull("We did not start with a focused vertex", focusedVertex); - - Collection vertices = functionGraph.getVertices(); - FGVertex otherVertex = null; - for (FGVertex vertex : vertices) { - if (vertex != focusedVertex) { - otherVertex = vertex; - break; - } - } - assertNotNull(otherVertex); - - Address address = otherVertex.getAddresses().getMinAddress(); - final ProgramLocation newVertexLocation = new ProgramLocation(program, address); - final FGController controller = - (FGController) TestUtils.getInstanceField("controller", graphProvider); - - runSwing(() -> controller.display(program, newVertexLocation)); - - // we must 'fake out' the listing to generate a location event from within the listing - pressRightArrowKey(otherVertex); - waitForSwing(); - - ProgramLocation codeBrowserLocation = runSwing(() -> codeBrowser.getCurrentLocation()); - ProgramLocation actualVertexLocation = otherVertex.getProgramLocation(); - assertEquals(newVertexLocation.getAddress(), actualVertexLocation.getAddress()); - assertEquals(actualVertexLocation.getAddress(), codeBrowserLocation.getAddress()); - } - - @Test - public void testFullyZoomedOutOption() throws Exception { - // - // Test the default option that fits the entire graph into the window. Then toggle the - // option and test that a new graph starts fully zoomed-in. - // - - hideSatellite();// for readability - setGraphWindowSize(300, 300);// make window small for easier testing - setZoomOutOption(true); - - assertZoomedOut(); - - setZoomOutOption(false); - - assertZoomedIn(); - } - - @Test - public void testNavigationHistory_VertexChangesOption() throws Exception { - - setNavigationHistoryOption(NavigationHistoryChoices.VERTEX_CHANGES); - - FGData graphData = getFunctionGraphData(); - FunctionGraph graph = graphData.getFunctionGraph(); - Collection vertices = graph.getVertices(); - - FGVertex start = getFocusedVertex(); - - Iterator it = vertices.iterator(); - FGVertex v1 = it.next(); - pickVertex(v1); - - FGVertex v2 = it.next(); - pickVertex(v2); - - FGVertex v3 = it.next(); - pickVertex(v3); - - assertInHistory(start, v1, v2); - } - - @Test - public void testNavigationHistory_NavigationEventsOption() throws Exception { - - setNavigationHistoryOption(NavigationHistoryChoices.NAVIGATION_EVENTS); - - clearHistory(); - - FGVertex v1 = vertex("01004178"); - pickVertex(v1); - - FGVertex v2 = vertex("01004192"); - pickVertex(v2); - - FGVertex v3 = vertex("010041a4"); - pickVertex(v3); - - // in this navigation mode, merely selecting nodes does *not* put previous nodes in history - assertNotInHistory(v1, v2); - - // - // Perform a navigation action (e.g., goTo()) and verify the old function is in the history - // - Address ghidra = getAddress("0x01002cf5"); - goTo(ghidra); - assertInHistory(v3.getVertexAddress()); - - Address foo = getAddress("0x01002339"); - goTo(foo); - assertInHistory(v3.getVertexAddress(), ghidra); - } - -//================================================================================================== -// Private Methods -//================================================================================================== - - private void assertNotInHistory(FGVertex... vertices) { - - List
vertexAddresses = - Arrays.stream(vertices) - .map(v -> v.getVertexAddress()) - .collect(Collectors.toList()); - assertNotInHistory(vertexAddresses); - } - - private void assertNotInHistory(List
addresses) { - - List locations = getNavigationHistory(); - List
actualAddresses = - locations.stream() - .map(memento -> memento.getProgramLocation().getAddress()) - .collect(Collectors.toList()); - - for (Address a : addresses) { - assertFalse("Vertex address should not be in the history list: " + a + ".\nHistory: " + - actualAddresses + "\nNavigated vertices: " + Arrays.asList(addresses), - actualAddresses.contains(a)); - } - } - - private void clearHistory() { - GoToService goTo = tool.getService(GoToService.class); - Navigatable navigatable = goTo.getDefaultNavigatable(); - - NavigationHistoryService service = tool.getService(NavigationHistoryService.class); - service.clear(navigatable); - } - - private List getNavigationHistory() { - - GoToService goTo = tool.getService(GoToService.class); - Navigatable navigatable = goTo.getDefaultNavigatable(); - - NavigationHistoryService service = tool.getService(NavigationHistoryService.class); - List locations = service.getPreviousLocations(navigatable); - return locations; - } - - private void assertInHistory(FGVertex... vertices) { - - List
vertexAddresses = - Arrays.stream(vertices) - .map(v -> v.getVertexAddress()) - .collect(Collectors.toList()); - assertInHistory(vertexAddresses); - } - - private void assertInHistory(Address... addresses) { - assertInHistory(Arrays.asList(addresses)); - } - - private void assertInHistory(List
expectedAddresses) { - - List actualLocations = getNavigationHistory(); - assertTrue( - "Vertex address should be in the history list: " + expectedAddresses + ".\nHistory: " + - actualLocations + "\nNavigated vertices: " + expectedAddresses, - expectedAddresses.size() <= actualLocations.size()); - - List
actualAddresses = - actualLocations.stream() - .map(memento -> memento.getProgramLocation().getAddress()) - .collect(Collectors.toList()); - - for (Address a : expectedAddresses) { - - assertTrue("Vertex address should be in the history list: " + a + ".\nActual: " + - actualAddresses + "\nExpected: " + expectedAddresses, - actualAddresses.contains(a)); - } - } - - private void setNavigationHistoryOption(NavigationHistoryChoices choice) throws Exception { - FGController controller = getFunctionGraphController(); - FunctionGraphOptions options = controller.getFunctionGraphOptions(); - runSwing(() -> setInstanceField("navigationHistoryChoice", options, choice)); - waitForSwing(); - } - - private void doTestLabelChangeAtVertexEntryUpdatesTitle() { - // get the graph contents - FGData graphData = getFunctionGraphData(); - assertNotNull(graphData); - assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults()); - - // locate vertex with cursor - Address vertexAddressWithDefaultLabel = getAddress("01004178"); - FunctionGraph graph = graphData.getFunctionGraph(); - FGVertex vertex = graph.getVertexForAddress(vertexAddressWithDefaultLabel); - String originalTitle = vertex.getTitle(); - - // add a label in the listing - String labelName = testName.getMethodName(); - AddLabelCmd addCmd = - new AddLabelCmd(vertexAddressWithDefaultLabel, labelName, SourceType.USER_DEFINED); - addCmd.applyTo(program); - program.flushEvents(); - waitForSwing(); - - // make sure the label appears in the vertex - String updatedTitle = vertex.getTitle(); - Assert.assertNotEquals(originalTitle, updatedTitle); - assertTrue(updatedTitle.indexOf(testName.getMethodName()) != -1); - } - - private void doTestRelayout(boolean fullReload) throws Exception { - - // - // This test covers navigation, which relies on the provider being focused to work - // - setProviderAlwaysFocused(); - - // - // Test that we can move a node, call relayout and that the moved node will not be - // at the moved position. - // - - FGData originalGraphData = getFunctionGraphData(); - assertNotNull(originalGraphData); - assertTrue("Unexpectedly received an empty FunctionGraphData", - originalGraphData.hasResults()); - - // - // Unusual Code: The initial load values may not be exactly the same as the values - // set during a relayout. To make sure that we are comparing apples-to-apples, - // we want to record the initial values after performing a relayout. Then, - // we change a node's position, relayout again, and check the final values - // with that after the first relayout. - // - if (fullReload) { - performReload(); - } - else { - performRelayout(); - } - - originalGraphData = getFunctionGraphData(); - FunctionGraph graph = originalGraphData.getFunctionGraph(); - final FGVertex rootVertex = graph.getRootVertex(); - assertNotNull(rootVertex); - - Point2D originalPoint = rootVertex.getLocation(); - - double dx = originalPoint.getX() + 100; - double dy = originalPoint.getY() + 100; - - Point2D newPoint = new Point2D.Double(dx, dy); - final Layout primaryLayout = getPrimaryLayout(); - final Point2D finalNewPoint = newPoint; - runSwing(() -> primaryLayout.setLocation(rootVertex, finalNewPoint)); - - assertEquals("Vertex location not correctly set", newPoint, rootVertex.getLocation()); - - // we have to wait for the paint to take place, as the rendering will change the vertex - // locations - FGPrimaryViewer primaryGraphViewer = getPrimaryGraphViewer(); - primaryGraphViewer.repaint(); - waitForSwing(); - - // move the location a bit, for later testing the sync between the listing and the graph - goToAddress("1004196"); - - // relayout - if (fullReload) { - performReload(); - } - else { - performRelayout(); - } - - FGData newGraphData = getFunctionGraphData(); - assertNotNull(newGraphData); - assertTrue("Unexpectedly received an empty FunctionGraphData", newGraphData.hasResults()); - - FunctionGraph newGraph = newGraphData.getFunctionGraph(); - FGVertex newRootVertex = newGraph.getRootVertex(); - assertNotNull(newRootVertex); - - waitForSwing(); - - // Note: we can't test for exact equality based upon the location we set, as the values - // are updated based upon other factors, like screen location and size. We want to make - // sure that the value is not the default. - Point2D reloadedPoint = newRootVertex.getLocation(); - assertTrue( - "Vertex location not restored to default after performing a relayout " + - "original point: " + originalPoint + " - reloaded point: " + reloadedPoint, - pointsAreSimilar(originalPoint, reloadedPoint)); - - // - // Make sure the CodeBrowser's location matches ours after the relayout (the location should - // get broadcast to the CodeBrowser) - // - - // Note: there is a timing failure that happens for this check; the event broadcast - // only happens if the FG provider has focus; in parallel batch mode focus is - // unreliable - if (!BATCH_MODE) { - assertTrue(graphAddressMatchesCodeBrowser(newGraph)); - } - } - - private boolean graphAddressMatchesCodeBrowser(FunctionGraph graph) { - FGVertex focusedVertex = runSwing(() -> graph.getFocusedVertex()); - ProgramLocation graphLocation = focusedVertex.getProgramLocation(); - ProgramLocation codeBrowserLocation = runSwing(() -> codeBrowser.getCurrentLocation()); - return graphLocation.getAddress().equals(codeBrowserLocation.getAddress()); - } -} diff --git a/Ghidra/Features/FunctionGraph/src/test/java/ghidra/app/plugin/core/functiongraph/FunctionGraphPlugin2Test.java b/Ghidra/Features/FunctionGraph/src/test/java/ghidra/app/plugin/core/functiongraph/FunctionGraphPlugin2Test.java deleted file mode 100644 index 3bd74b9fac..0000000000 --- a/Ghidra/Features/FunctionGraph/src/test/java/ghidra/app/plugin/core/functiongraph/FunctionGraphPlugin2Test.java +++ /dev/null @@ -1,841 +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.app.plugin.core.functiongraph; - -import static ghidra.graph.viewer.GraphViewerUtils.*; -import static org.junit.Assert.*; - -import java.awt.Color; -import java.awt.Point; -import java.util.*; - -import javax.swing.JComponent; - -import org.junit.*; - -import docking.action.DockingActionIf; -import docking.theme.GThemeDefaults.Colors.Palette; -import edu.uci.ics.jung.graph.Graph; -import generic.test.TestUtils; -import ghidra.app.cmd.label.AddLabelCmd; -import ghidra.app.cmd.label.DeleteLabelCmd; -import ghidra.app.cmd.refs.AddMemRefCmd; -import ghidra.app.cmd.refs.RemoveReferenceCmd; -import ghidra.app.plugin.core.colorizer.ColorizingPlugin; -import ghidra.app.plugin.core.colorizer.ColorizingService; -import ghidra.app.plugin.core.functiongraph.graph.*; -import ghidra.app.plugin.core.functiongraph.graph.vertex.*; -import ghidra.app.plugin.core.functiongraph.mvc.*; -import ghidra.app.plugin.core.navigation.GoToAddressLabelPlugin; -import ghidra.app.plugin.core.navigation.NextPrevAddressPlugin; -import ghidra.app.util.viewer.listingpanel.ListingPanel; -import ghidra.graph.viewer.GraphPerspectiveInfo; -import ghidra.program.model.address.Address; -import ghidra.program.model.address.AddressSetView; -import ghidra.program.model.symbol.*; -import ghidra.program.util.ProgramLocation; -import ghidra.program.util.ProgramSelection; -import ghidra.util.Msg; - -public class FunctionGraphPlugin2Test extends AbstractFunctionGraphTest { - - @Override - @Before - public void setUp() throws Exception { - super.setUp(); - - tool.addPlugin(GoToAddressLabelPlugin.class.getName()); - tool.addPlugin(NextPrevAddressPlugin.class.getName()); - - waitForGraphToLoad(); - } - - @Test - public void testSelectionFromCodeBrowser() { - FGData graphData = getFunctionGraphData(); - assertNotNull(graphData); - assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults()); - ProgramLocation location = getLocationForAddressString(startAddressString); - assertTrue(graphData.containsLocation(location)); - FunctionGraph functionGraph = graphData.getFunctionGraph(); - - // locate vertex with cursor - FGVertex focusedVertex = getFocusVertex(functionGraph); - assertNotNull("We did not start with a focused vertex", focusedVertex); - assertTrue(focusedVertex.containsProgramLocation(location)); - - // make a selection starting at the current location - ProgramSelection ps = makeSingleVertexSelectionInCodeBrowser(); - ProgramSelection vertexSelection = focusedVertex.getProgramSelection(); - assertTrue(!ps.isEmpty()); - assertEquals("A selection from the code browser that is completely contained in the " + - "tested vertex is not in the vertex", ps, vertexSelection); - } - - @Test - public void testRedockSatellite() { - - showSatellite();// make sure it is on - - undockSatellite(); - redockSatellite(); - assertNoUndockedProvider(); - assertSatelliteVisible(true); - } - - @Test - public void testUndockSatellite() { - - showSatellite();// make sure it is on - - assertNoUndockedProvider(); - - undockSatellite(); - - assertSatelliteVisible(true); - assertUndockedProviderShowing(); - } - - @Test - public void testShowSatelliteButtonWhenDocked() { - showSatellite();// make sure it is on - - toggleSatalliteVisible(false); - assertSatelliteVisible(false); - - pressShowSatelliteButton(); - - assertSatelliteVisible(true); - assertNoUndockedProvider(); - } - - @Test - public void testShowSatelliteButtonWhenUnDocked() { - showSatellite();// make sure it is on - - undockSatellite(); - assertUndockedProviderShowing(); - - closeUndockedProvider(); - assertUndockedProviderNotShowing(); - - pressShowSatelliteButton(); - - assertUndockedProviderShowing(); - assertSatelliteVisible(true); - } - - @Test - public void testUndockWhileInvisible() { - - toggleSatalliteVisible(false); - assertSatelliteVisible(false); - - undockSatellite(); - assertUndockedProviderShowing(); - - redockSatellite(); - - // note: redocking the satellite will make it visible again - assertSatelliteVisible(true); - } - - @Test - public void testSnapshotWithUndockedSatellite() { - - undockSatellite(); - - FGController newController = cloneGraph(); - assertUndockedProviderShowing(newController.getProvider()); - isSatelliteVisible(newController); - } - - @SuppressWarnings("unchecked") - // list cast - @Test - public void testSnapshot() { - List disconnectedProviders = - (List) getInstanceField("disconnectedProviders", graphPlugin); - assertTrue(disconnectedProviders.isEmpty()); - - FGController primaryController = getFunctionGraphController(); - waitForBusyRunManager(primaryController); - ProgramLocation location = graphProvider.getLocation(); - GraphPerspectiveInfo primaryPerspective = - primaryController.getGraphPerspective(location); - - DockingActionIf snapshotAction = - getAction(tool, graphPlugin.getName(), "Function Graph Clone"); - performAction(snapshotAction, true); - - assertEquals(1, disconnectedProviders.size()); - FGProvider providerClone = disconnectedProviders.get(0); - FGController controllerClone = (FGController) getInstanceField("controller", providerClone); - waitForBusyRunManager(controllerClone); - ProgramLocation cloneLocation = providerClone.getLocation(); - GraphPerspectiveInfo clonePerspective = - controllerClone.getGraphPerspective(cloneLocation); - - double primaryPerspectiveZoom = primaryPerspective.getZoom(); - double clonePerspectiveZoom = clonePerspective.getZoom(); - assertEquals(primaryPerspectiveZoom, clonePerspectiveZoom, .001); - - Point primaryPoint = primaryPerspective.getLayoutTranslateCoordinates(); - Point clonePoint = clonePerspective.getLayoutTranslateCoordinates(); - - assertPointsAreAboutEqual("Cloned graph view info does not match the source graph", - primaryPoint, clonePoint); - } - - @Test - public void testZoom() { - setZoom(0.5d); - - waitForAnimation(); - - FGPrimaryViewer primaryGraphViewer = getPrimaryGraphViewer(); - Double originalGraphScale = getGraphScale(primaryGraphViewer); - Msg.debug(this, "original scale: " + originalGraphScale); - - // zoom at code level - Msg.debug(this, "zooming in..."); - zoomInCompletely(); - Double zoomedInGraphScale = getGraphScale(primaryGraphViewer); - Msg.debug(this, "new scale: " + zoomedInGraphScale); - - Assert.assertNotEquals(originalGraphScale, zoomedInGraphScale); - } - - @Test - public void testSplitAndMergeNodesOnStaleGraph_ForReference() { - int txID = -1; - try { - txID = program.startTransaction("Test: " + testName.getMethodName()); - doTestSplitAndMergeNodesOnStaleGraph_ForReference(); - } - finally { - program.endTransaction(txID, false); - } - } - - @Test - public void testSplitAndMergeNodesOnStaleGraph_ForSymbol() { - int txID = -1; - try { - txID = program.startTransaction("Test: " + testName.getMethodName()); - doTestSplitAndMergeNodesOnStaleGraph_ForSymbol(); - } - finally { - program.endTransaction(txID, false); - } - } - - @Test - public void testSetVertexColor() { - FGVertex focusedVertex = getFocusedVertex(); - - JComponent panel = focusedVertex.getComponent(); - ListingPanel listingPanel = - (ListingPanel) TestUtils.getInstanceField("listingPanel", panel); - Color startBackgrond = listingPanel.getTextBackgroundColor(); - Color testColor = Palette.RED; - assertTrue("Unexpected start color--must change the test!", - !testColor.equals(startBackgrond)); - - chooseColor(focusedVertex, testColor); - - Color newBackground = listingPanel.getTextBackgroundColor(); - assertTrue(!startBackgrond.equals(newBackground)); - } - - @Test - public void testSharedColorExperience() throws Exception { - // - // Tests the new way of coloring vertices, by way of the ColorizerService, which will - // set the color in both the vertex and the listing (really just in the listing, but - // the vertex displays this color. - // - - // install ColorizerPlugin - tool.addPlugin(ColorizingPlugin.class.getName()); - - FGVertex vertex = getFocusedVertex(); - ColorizingService colorizingService = tool.getService(ColorizingService.class); - Color appliedBackgroundColor = - colorizingService.getBackgroundColor(vertex.getVertexAddress()); - - Color testColor = Palette.RED; - assertTrue("Unexpected start color--must change the test!", - !testColor.equals(appliedBackgroundColor)); - - chooseColor(vertex, testColor); - - // make sure the service is also cognizant of the color change - appliedBackgroundColor = colorizingService.getBackgroundColor(vertex.getVertexAddress()); - assertEquals(testColor, appliedBackgroundColor); - - Color vBg = vertex.getBackgroundColor(); - assertEquals(appliedBackgroundColor, vBg); - - // - // Reload and make sure the color is re-applied to the vertex (this was broken) - // - Address vertexAddress = vertex.getVertexAddress(); - performReload(); - FGVertex reloadedVertex = vertex(vertexAddress); - assertNotSame(vertex, reloadedVertex); - vBg = reloadedVertex.getBackgroundColor(); - assertEquals(appliedBackgroundColor, vBg); - } - - @Test - public void testSetMostRecentColorAction() throws Exception { - // - // Test that the 'set most recent color' action will set the color of the vertex *and* - // in the Listing. - // - // install ColorizerPlugin - tool.addPlugin(ColorizingPlugin.class.getName()); - - FGVertex focusedVertex = getFocusedVertex(); - ColorizingService colorizingService = tool.getService(ColorizingService.class); - Color startBackgroundColor = - colorizingService.getBackgroundColor(focusedVertex.getVertexAddress()); - - FGController controller = getFunctionGraphController(); - Color mostRecentColor = controller.getMostRecentColor(); - - Assert.assertNotEquals( - "Test environment not setup correctly--should have default backgrond " + - "colors applied", - startBackgroundColor, mostRecentColor); - - SetVertexMostRecentColorAction setRecentColorAction = - getSetMostRecentColorAction(focusedVertex); - performAction(setRecentColorAction, graphProvider, true); - - Color newVertexBackgroundColor = focusedVertex.getBackgroundColor(); - assertEquals("'Set Most Recent Color' action did not apply that color to the vertex", - mostRecentColor, newVertexBackgroundColor); - - Color newBackgroundColor = - colorizingService.getBackgroundColor(focusedVertex.getVertexAddress()); - assertEquals("'Set Most Recent Color' action did not apply that color to the color service", - mostRecentColor, newBackgroundColor); - } - - // TODO: see SCR 9208 - we don't currently support this, although we could - public void dont_testNavigatingBackwardsRestoresPerspectiveInfo_ZoomOutOn() throws Exception { - // - // Test, that with the default 'zoomed-out' option *on* for new graphs, we will restore the - // user's previous graph perspective data when they navigate back to a function that - // were examining (instead of zooming back out to the full view). - // - - hideSatellite();// for readability - setGraphWindowSize(300, 300);// make window small for easier testing - - setZoomOutOption(true); - - // zoom in to a vertex (pick something beside the entry point) - goTo("1004196"); - zoomInCompletely(); - - // move that vertex off center - moveView(10); - FGVertex v = getFocusedVertex(); - Point originalLocation = getPointInViewSpaceForVertex(getPrimaryGraphViewer(), v); - - // 'go to' a new function to change the graph - goTo("01002239"); - - // trigger a back navigation - navigateBack(); - - // make sure that the zoom is restored (note: the point information reflects both - // location and zoom) - Point restoredLocation = getPointInViewSpaceForVertex(getPrimaryGraphViewer(), v); - assertEquals(originalLocation, restoredLocation); - } - - // not sure how to trigger a selection from a node via the GUI...if we use the API method, - // as it does now, the CodeBrowser does not get the callback, because that's how the system works. - // If it is worth testing, and we can do it from the mouse, then have at it - public void dont_testSelectionFromGraphToCodeBrowser() { - FGData graphData = getFunctionGraphData(); - assertNotNull(graphData); - assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults()); - ProgramLocation location = getLocationForAddressString(startAddressString); - assertTrue(graphData.containsLocation(location)); - FunctionGraph functionGraph = graphData.getFunctionGraph(); - - // locate vertex with cursor - FGVertex focusedVertex = getFocusVertex(functionGraph); - assertNotNull("We did not start with a focused vertex", focusedVertex); - - AddressSetView addresses = focusedVertex.getAddresses(); - Address address = addresses.getMinAddress(); - focusedVertex.setProgramSelection(new ProgramSelection(address, address)); - - // make sure the code browser now contains a matching selection - ProgramSelection firstSelection = focusedVertex.getProgramSelection(); - ListingPanel listingPanel = codeBrowser.getListingPanel(); - ProgramSelection codeBrowserSelection = listingPanel.getProgramSelection(); - assertTrue(!firstSelection.isEmpty()); - assertEquals("Selecting text in the vertex did not select text in the code browser", - firstSelection, codeBrowserSelection); - - Collection vertices = functionGraph.getVertices(); - FGVertex otherVertex = null; - for (FGVertex vertex : vertices) { - if (vertex != focusedVertex) { - otherVertex = vertex; - break; - } - } - assertNotNull(otherVertex); - - Address otherAddress = otherVertex.getAddresses().getMinAddress(); - otherVertex.setProgramSelection(new ProgramSelection(otherAddress, otherAddress)); - ProgramSelection secondSelection = otherVertex.getProgramSelection(); - assertTrue(!secondSelection.isEmpty()); - - codeBrowserSelection = listingPanel.getProgramSelection(); - - // the new code browser selection should have both of our vertex selections - assertTrue(codeBrowserSelection.getMinAddress().equals(firstSelection.getMinAddress())); - assertTrue(codeBrowserSelection.getMaxAddress().equals(secondSelection.getMaxAddress())); - } - - // TODO: see SCR 9208 - we don't currently support this, although we could - public void dontTestNavigatingBackwardsRestoresPerspectiveInfo_ZoomOutOff() throws Exception { - // - // Test, that with the default 'zoomed-out' option *off* for new graphs, we will restore the - // user's previous graph perspective data when they navigate back to a function that - // were examining (instead of zooming back out to the full view). - // - - hideSatellite();// for readability - setGraphWindowSize(300, 300);// make window small for easier testing - - setZoomOutOption(false); - - // zoom in to a vertex (pick something beside the entry point) - goTo("1004196"); - zoomInCompletely(); - - // move that vertex off center - moveView(10); - FGVertex v = getFocusedVertex(); - Point originalLocation = getPointInViewSpaceForVertex(getPrimaryGraphViewer(), v); - - // 'go to' a new function to change the graph - goTo("01002239"); - - // trigger a back navigation - navigateBack(); - - // make sure that the zoom is restored (note: the point information reflects both - // location and zoom) - Point restoredLocation = getPointInViewSpaceForVertex(getPrimaryGraphViewer(), v); - assertEquals(originalLocation, restoredLocation); - } - - // - // TODO: I have changed the groups such that you can group a single node, which lets you - // replace the view of the node with user-defined text. - // - // This tests verifies that a group will not be created if there is only one vertex - // found upon restoring settings. If we want to put that code back, then this test - // is again valid. - // - @Override - public void dontTestRestoringWhenCodeBlocksHaveChanged_DoesntRegroup() { - int transactionID = -1; - try { - transactionID = program.startTransaction(testName.getMethodName()); - doTestRestoringWhenCodeBlocksHaveChanged_DoesntRegroup(); - } - finally { - program.endTransaction(transactionID, false); - } - } - - public void doTestSplitAndMergeNodesOnStaleGraph_ForReference() { - // - // Test that we can split a node into two for reference operations: - // 1) Adding a reference splits a node - // 2) Removing a reference merges a node - // 2) The above actions do not take place with automatic updates on - // - // Edges cases: - // 1) Adding a reference to a location already containing a symbol does not split the node - // 2) Removing a reference with other references at the vertex entry point does not merge nodes - // - - // Find a good test function. - goToAddress("01002cf5"); - - // Find a node to manipulate - FGData graphData = getFunctionGraphData(); - assertNotNull(graphData); - assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults()); - - String vertexAddressString = "01002d2b"; - Address vertexAddress = getAddress(vertexAddressString); - ProgramLocation location = getLocationForAddressString(vertexAddressString); - assertTrue(graphData.containsLocation(location)); - FunctionGraph functionGraph = graphData.getFunctionGraph(); - Graph graph = functionGraph; - FGVertex initialVertex = functionGraph.getVertexForAddress(vertexAddress); - - // Find a location within that node to which we will add a label - String referenceFromAddress = "01002d3d"; - String referenceToAddress = "01002d47"; - Address fromAddress = getAddress(referenceFromAddress); - Address toAddress = getAddress(referenceToAddress); - - // - // Start with case 1 -- adding a reference - // - // Verify the graph is not stale - FGController controller = getFunctionGraphController(); - FGView view = controller.getView(); - assertTrue(!view.isGraphViewStale()); - - // Add a reference - AddMemRefCmd addCmd = - new AddMemRefCmd(fromAddress, toAddress, RefType.DATA, SourceType.USER_DEFINED, -1); - addCmd.applyTo(program); - program.flushEvents(); - waitForSwing(); - - waitForAnimation(); - - // Verify the edited node has been split--the old vertex is gone; two new vertices exist - assertTrue(!graph.containsVertex(initialVertex)); - FGVertex newParentVertex = functionGraph.getVertexForAddress(fromAddress); - assertNotNull(newParentVertex); - - FGVertex newChildVertex = functionGraph.getVertexForAddress(toAddress); - assertNotNull(newChildVertex); - - Collection parentOutEdges = graph.getOutEdges(newParentVertex); - assertTrue(parentOutEdges.size() == 1); - assertEquals(newChildVertex, parentOutEdges.iterator().next().getEnd()); - - // Verify the graph is stale - assertTrue(view.isGraphViewStale()); - - // Now remove the reference - ReferenceManager referenceManager = program.getReferenceManager(); - Reference reference = referenceManager.getReference(fromAddress, toAddress, -1); - assertNotNull(reference); - RemoveReferenceCmd deleteCmd = new RemoveReferenceCmd(reference); - deleteCmd.applyTo(program); - program.flushEvents(); - waitForSwing(); - waitForAnimation(); - - // Verify the new nodes have been merged - assertTrue(!graph.containsVertex(newParentVertex)); - assertTrue(!graph.containsVertex(newChildVertex)); - - FGVertex newOldVertex = functionGraph.getVertexForAddress(vertexAddress); - assertNotNull(newParentVertex); - - FGVertex newOldChildVertex = functionGraph.getVertexForAddress(toAddress); - assertNotNull(newChildVertex); - - assertEquals(newOldVertex, newOldChildVertex);// this should be the same after the merge - - // - // Edge Cases - // - - // Relayout to put the graph in a known good state - performRelayout(); - - // Find a node to manipulate (one that already has a reference) - referenceFromAddress = "01002d29"; - referenceToAddress = "01002d52"; - fromAddress = getAddress(referenceFromAddress); - toAddress = getAddress(referenceToAddress); - FGVertex fromVertex = functionGraph.getVertexForAddress(vertexAddress); - assertTrue(graph.containsVertex(fromVertex)); - - // Add a reference to the entry point of the node - addCmd = - new AddMemRefCmd(fromAddress, toAddress, RefType.DATA, SourceType.USER_DEFINED, -1); - addCmd.applyTo(program); - program.flushEvents(); - waitForSwing(); - waitForAnimation(); - - // Verify the node was not split - assertTrue(graph.containsVertex(fromVertex)); - - // Now remove the reference we added - reference = referenceManager.getReference(fromAddress, toAddress, -1); - assertNotNull(reference); - deleteCmd = new RemoveReferenceCmd(reference); - deleteCmd.applyTo(program); - program.flushEvents(); - waitForSwing(); - waitForAnimation(); - - // Verify the node was not merged - assertTrue(graph.containsVertex(fromVertex)); - } - - protected void doTestRestoringWhenCodeBlocksHaveChanged_DoesntRegroup() { - // - // Tests the behavior of how group vertices are restored when one or more of the vertices - // inside of the grouped vertex is no longer available when the graph attempts to restore - // the group vertex user settings (i.e., when restarting Ghidra, the previously grouped - // vertices should reappear). - // - // In this test, we will be mutating a group of 2 nodes such - // that one of the nodes has been split into two. This leaves only one vertex to - // be found by the regrouping algorithm. Furthermore, the regrouping will not take place - // if at least two vertices cannot be found. - // - - // - // Pick a function and group some nodes. - // - FGData graphData = graphFunction("01002cf5"); - FunctionGraph functionGraph = graphData.getFunctionGraph(); - - Set ungroupedVertices = - selectVertices(functionGraph, "01002d11" /* LAB_01002d11 */, "01002cf5" /* ghidra */); - - group(ungroupedVertices); - - // 5 edges expected: - // -01002cf5: 2 out - // -01002cf5: 2 in, 1 out - int expectedGroupedEdgeCount = 5; - GroupedFunctionGraphVertex groupedVertex = validateNewGroupedVertexFromVertices( - functionGraph, ungroupedVertices, expectedGroupedEdgeCount); - - AddressSetView addresses = groupedVertex.getAddresses(); - Address minAddress = addresses.getMinAddress(); - - // - // Ideally, we would like to save, close and re-open the program so that we can get - // a round-trip saving and reloading. However, in the test environment, we cannot save - // our programs. So, we will instead just navigate away from the current function, clear - // the cache (to make sure that we read the settings again), and then verify that the - // data saved in the program has been used to re-group. - // - graphFunction("0100415a"); - clearCache(); - - // - // Add a label to trigger a code block change - // - createLabel("01002d18");// in the middle of the LAB_01002d11 code block - - // - // Relaunch the graph, which will use the above persisted group settings... - // - graphData = graphFunction("01002cf5"); - waitForAnimation();// the re-grouping may be using animation, which runs after the graph is loaded - functionGraph = graphData.getFunctionGraph(); - FGVertex expectedGroupVertex = functionGraph.getVertexForAddress(minAddress); - assertFalse(expectedGroupVertex instanceof GroupedFunctionGraphVertex); - } - - protected void doTestSplitAndMergeNodesOnStaleGraph_ForSymbol() { - // - // Test that we can split a node into two for symbol operations: - // 1) Adding a symbol splits a node - // 2) Removing a symbol merges a node - // 2) The above actions do not take place with automatic updates on - // - // Edges cases: - // 1) Adding a symbol to a location already containing a symbol does not split the node - // 2) Removing a symbol with other symbols at the vertex entry point does not merge nodes - // - - // Find a good test function. - goToAddress("01002cf5"); - - // Find a node to manipulate - FGData graphData = getFunctionGraphData(); - assertNotNull(graphData); - assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults()); - - String vertexAddressString = "01002d2b"; - Address vertexAddress = getAddress(vertexAddressString); - ProgramLocation location = getLocationForAddressString(vertexAddressString); - assertTrue(graphData.containsLocation(location)); - FunctionGraph functionGraph = graphData.getFunctionGraph(); - Graph graph = functionGraph; - FGVertex initialVertex = functionGraph.getVertexForAddress(vertexAddress); - - // Find a location within that node to which we will add a label - String labelAddressString = "01002d47"; - Address labelAddress = getAddress(labelAddressString); - - // - // Start with case 1 -- adding a symbol - // - // Verify the graph is not stale - FGController controller = getFunctionGraphController(); - FGView view = controller.getView(); - assertTrue(!view.isGraphViewStale()); - - // Add a label - String labelName = testName.getMethodName(); - AddLabelCmd addCmd = new AddLabelCmd(labelAddress, labelName, SourceType.USER_DEFINED); - addCmd.applyTo(program); - program.flushEvents(); - waitForSwing(); - waitForAnimation(); - - // Verify the edited node has been split--the old vertex is gone; two new vertices exist - assertTrue(!graph.containsVertex(initialVertex)); - FGVertex newParentVertex = functionGraph.getVertexForAddress(vertexAddress); - assertNotNull(newParentVertex); - - FGVertex newChildVertex = functionGraph.getVertexForAddress(labelAddress); - assertNotNull(newChildVertex); - - Collection parentOutEdges = graph.getOutEdges(newParentVertex); - assertTrue(parentOutEdges.size() == 1); - assertEquals(newChildVertex, parentOutEdges.iterator().next().getEnd()); - - // Verify the graph is stale - assertTrue(view.isGraphViewStale()); - - // Now remove the label - DeleteLabelCmd deleteCmd = new DeleteLabelCmd(labelAddress, labelName); - deleteCmd.applyTo(program); - program.flushEvents(); - waitForSwing(); - waitForAnimation(); - - // Verify the new nodes have been merged - assertTrue(!graph.containsVertex(newParentVertex)); - assertTrue(!graph.containsVertex(newChildVertex)); - - FGVertex newOldVertex = functionGraph.getVertexForAddress(vertexAddress); - assertNotNull(newParentVertex); - - FGVertex newOldChildVertex = functionGraph.getVertexForAddress(labelAddress); - assertNotNull(newChildVertex); - - assertEquals(newOldVertex, newOldChildVertex);// this should be the same after the merge - - // Verify the graph is still stale - assertTrue(view.isGraphViewStale()); - - // - // Edge Cases - // - - // Relayout to put the graph in a known good state - performRelayout(); - - // Find a node to manipulate (one that already has a label) - FGVertex vertexWithLabel = functionGraph.getVertexForAddress(vertexAddress); - - // Add a label to the entry point of the node - labelName = testName.getMethodName() + "2"; - addCmd = new AddLabelCmd(vertexAddress, labelName, SourceType.USER_DEFINED); - addCmd.applyTo(program); - program.flushEvents(); - waitForSwing(); - waitForAnimation(); - - // Verify the node was not split - assertTrue(graph.containsVertex(vertexWithLabel)); - - // Verify the graph is stale - assertTrue(view.isGraphViewStale()); - - // Now remove the label we added - deleteCmd = new DeleteLabelCmd(vertexAddress, labelName); - deleteCmd.applyTo(program); - program.flushEvents(); - waitForSwing(); - waitForAnimation(); - - // Verify the node was not merged - assertTrue(graph.containsVertex(vertexWithLabel)); - } - - // note: unreliable--garbage collection works differently across platforms (sad face) -// public void testClearCacheAndMemoryLeak() { -// // -// // Test that when we clear the cache of a graph the vertices of that graph will be -// // garbage collected -// // -// WeakSet weakSet = -// WeakDataStructureFactory.createSingleThreadAccessWeakSet(); - // -// FunctionGraphData graphData = getFunctionGraphData(); -// assertNotNull(graphData); -// assertTrue("Unexpectedly received an empty FunctionGraphData", graphData.hasResults()); - // -// FunctionGraph graph = graphData.getFunctionGraph(); -// FunctionGraphVertex rootVertex = graph.getRootVertex(); -// assertNotNull(rootVertex); - // -// weakSet.add(rootVertex); -// assertTrue(weakSet.iterator().hasNext()); - // -// // move to a new function so that we the FG will not be holding a reference to the -// // originally graphed function -// String address = "01002239"; -// goToAddress(address); - // -// FunctionGraphData newGraphData = getFunctionGraphData(); -// assertNotNull(newGraphData); -// assertTrue("Unexpectedly received an empty FunctionGraphData", newGraphData.hasResults()); - // -// assertTrue( -// "Function Graph did not graph a new function as expected at address: " + address, -// !graphData.equals(newGraphData)); - // -// triggerGraphDisposal(graphData); - // -// rootVertex = null; -// graph = null; -// graphData = null; - // -// // let (force) the Garbage Collector run -// System.gc(); -// sleep(100); -// System.gc(); -// sleep(100); -// System.gc(); -// sleep(100); - // -// boolean isNotCollected = weakSet.iterator().hasNext(); -// assertFalse(isNotCollected); -// } - // -// private void triggerGraphDisposal(FunctionGraphData dataToDispose) { -// Object controller = getInstanceField("controller", graphProvider); -// LRUMap cache = (LRUMap) getInstanceField("graphCache", controller); -// cache.clear(); -// dataToDispose.dispose(); -// } - -} diff --git a/Ghidra/Framework/Docking/certification.manifest b/Ghidra/Framework/Docking/certification.manifest index 6efff61d3a..a3b266df21 100644 --- a/Ghidra/Framework/Docking/certification.manifest +++ b/Ghidra/Framework/Docking/certification.manifest @@ -21,6 +21,7 @@ src/main/help/help/shared/tip.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme ( src/main/help/help/shared/undo.png||GHIDRA||||END| src/main/help/help/shared/warning.png||Oxygen Icons - LGPL 3.0|||Oxygen icon theme (dual license; LGPL or CC-SA-3.0)|END| src/main/help/help/topics/PlacheholderTopic/Placeholder.htm||GHIDRA||||END| +data/docking.palette.material.theme.properties||GHIDRA||||END| data/docking.palette.theme.properties||GHIDRA||||END| data/docking.theme.properties||GHIDRA||||END| src/main/java/docking/dnd/package.html||GHIDRA||reviewed||END| diff --git a/Ghidra/Framework/Docking/data/docking.palette.material.theme.properties b/Ghidra/Framework/Docking/data/docking.palette.material.theme.properties new file mode 100644 index 0000000000..1751a1e2a9 --- /dev/null +++ b/Ghidra/Framework/Docking/data/docking.palette.material.theme.properties @@ -0,0 +1,14 @@ +// TODO using this now as a placeholder for a palette/swatch + +[Defaults] + + +color.palette.material.primary = #6200EE +color.palette.material.secondary = #03DAC6 +color.palette.material.secondary.variant = #018786 + +[Dark Defaults] + +color.palette.material.primary = #BB86FC +color.palette.material.secondary = #03DAC6 +color.palette.material.secondary.variant = #018786 \ No newline at end of file diff --git a/Ghidra/Framework/Docking/data/docking.palette.theme.properties b/Ghidra/Framework/Docking/data/docking.palette.theme.properties index 4c505e9a26..dd5efe1d8e 100644 --- a/Ghidra/Framework/Docking/data/docking.palette.theme.properties +++ b/Ghidra/Framework/Docking/data/docking.palette.theme.properties @@ -8,6 +8,7 @@ color.palette.crimson = crimson color.palette.cyan = cyan color.palette.darkblue = DarkBlue color.palette.darkkhaki = DarkKhaki +color.palette.darkred = DarkRed color.palette.dodgerblue = DodgerBlue color.palette.gold = gold color.palette.gray = gray diff --git a/Ghidra/Framework/Docking/src/main/java/docking/DefaultHelpService.java b/Ghidra/Framework/Docking/src/main/java/docking/DefaultHelpService.java new file mode 100644 index 0000000000..34bd0ec52b --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/DefaultHelpService.java @@ -0,0 +1,118 @@ +/* ### + * 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; + +import java.awt.*; + +import javax.swing.JButton; + +import docking.help.HelpDescriptor; +import docking.help.HelpService; +import ghidra.util.HelpLocation; +import ghidra.util.Msg; + +public class DefaultHelpService implements HelpService { + + @Override + public void showHelp(Object helpObj, boolean infoOnly, Component parent) { + if (infoOnly) { + displayHelpInfo(helpObj); + return; + } + } + + @Override + public void showHelp(java.net.URL url) { + // no-op + } + + @Override + public void showHelp(HelpLocation location) { + // no-op + } + + @Override + public void excludeFromHelp(Object helpObject) { + // no-op + } + + @Override + public boolean isExcludedFromHelp(Object helpObject) { + return false; + } + + @Override + public void clearHelp(Object helpObject) { + // no-op + } + + @Override + public void registerHelp(Object helpObj, HelpLocation helpLocation) { + // no-op + } + + @Override + public HelpLocation getHelpLocation(Object object) { + return null; + } + + @Override + public boolean helpExists() { + return false; + } + + @Override + public void reload() { + // no-op + } + + private void displayHelpInfo(Object helpObj) { + String msg = getHelpInfo(helpObj); + Msg.showInfo(this, null, "Help Info", msg); + } + + private String getHelpInfo(Object helpObj) { + if (helpObj == null) { + return "Help Object is null"; + } + StringBuilder buffy = new StringBuilder(); + buffy.append("HELP OBJECT: " + helpObj.getClass().getName()); + buffy.append("\n"); + if (helpObj instanceof HelpDescriptor) { + HelpDescriptor helpDescriptor = (HelpDescriptor) helpObj; + buffy.append(helpDescriptor.getHelpInfo()); + + } + else if (helpObj instanceof JButton) { + JButton button = (JButton) helpObj; + buffy.append(" BUTTON: " + button.getText()); + buffy.append("\n"); + Component c = button; + while (c != null && !(c instanceof Window)) { + c = c.getParent(); + } + if (c instanceof Dialog) { + buffy.append(" DIALOG: " + ((Dialog) c).getTitle()); + buffy.append("\n"); + } + if (c instanceof Frame) { + buffy.append(" FRAME: " + ((Frame) c).getTitle()); + buffy.append("\n"); + } + } + return buffy.toString(); + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/action/ComponentThemeInspectorAction.java b/Ghidra/Framework/Docking/src/main/java/docking/action/ComponentThemeInspectorAction.java index 0b43938cd8..344cb8a2d9 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/action/ComponentThemeInspectorAction.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/action/ComponentThemeInspectorAction.java @@ -16,7 +16,6 @@ package docking.action; import java.awt.*; -import java.awt.event.KeyEvent; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -28,15 +27,16 @@ import javax.swing.tree.TreePath; import org.apache.commons.lang3.StringUtils; -import docking.*; +import docking.ActionContext; +import docking.DockingWindowManager; import ghidra.util.Msg; +import ghidra.util.ReservedKeyBindings; public class ComponentThemeInspectorAction extends DockingAction { public ComponentThemeInspectorAction() { super("Component Theme Inspector", DockingWindowManager.DOCKING_WINDOWS_OWNER, false); - createReservedKeyBinding( - KeyStroke.getKeyStroke(KeyEvent.VK_F9, DockingUtils.CONTROL_KEY_MODIFIER_MASK)); + createReservedKeyBinding(ReservedKeyBindings.COMPONENT_THEME_INFO_KEY); // System action; no help needed DockingWindowManager.getHelpService().excludeFromHelp(this); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/action/ShowFocusInfoAction.java b/Ghidra/Framework/Docking/src/main/java/docking/action/ShowFocusInfoAction.java index 806dcbd6fa..a3a2c1d541 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/action/ShowFocusInfoAction.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/action/ShowFocusInfoAction.java @@ -82,6 +82,7 @@ public class ShowFocusInfoAction extends DockingAction { Object mouseOverObject = DockingWindowManager.getMouseOverObject(); if (mouseOverObject instanceof Component) { log.info("Mouse-over Object: " + printComp((Component) mouseOverObject)); + log.info("Focusable?: " + ((Component) mouseOverObject).isFocusable()); } log.info(""); } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/help/CustomTOCView.java b/Ghidra/Framework/Docking/src/main/java/docking/help/CustomTOCView.java new file mode 100644 index 0000000000..ff6b4e3ae9 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/help/CustomTOCView.java @@ -0,0 +1,527 @@ +/* ### + * 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.help; + +import java.awt.Component; +import java.net.MalformedURLException; +import java.net.URL; +import java.util.Hashtable; +import java.util.Locale; + +import javax.help.*; +import javax.help.Map.ID; +import javax.help.event.HelpModelEvent; +import javax.help.plaf.HelpNavigatorUI; +import javax.help.plaf.basic.BasicTOCCellRenderer; +import javax.help.plaf.basic.BasicTOCNavigatorUI; +import javax.swing.*; +import javax.swing.event.TreeSelectionEvent; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.TreePath; + +import ghidra.util.Msg; +import ghidra.util.SystemUtilities; + +/** + * A custom Table of Contents view that we specify in our JavaHelp xml documents. This view + * lets us install custom renderers and custom tree items for use by those renderers. These + * renderers let us display custom text defined by the TOC_Source.xml files. We also add some + * utility like: tooltips in development mode, node selection when pressing F1. + */ +public class CustomTOCView extends TOCView { + + private CustomTOCNavigatorUI ui; + + private boolean isSelectingNodeInternally; + + // Hashtable + public CustomTOCView(HelpSet hs, String name, String label, + @SuppressWarnings("rawtypes") Hashtable params) { + this(hs, name, label, hs.getLocale(), params); + } + + // Hashtable + public CustomTOCView(HelpSet hs, String name, String label, Locale locale, + @SuppressWarnings("rawtypes") Hashtable params) { + super(hs, name, label, locale, params); + } + + @Override + // overrode this method to install our custom UI, which lets us use our custom renderer + public Component createNavigator(HelpModel model) { + JHelpTOCNavigator helpTOCNavigator = new JHelpTOCNavigator(this, model) { + @Override + public void setUI(HelpNavigatorUI newUI) { + CustomTOCView.this.ui = new CustomTOCNavigatorUI(this); + super.setUI(CustomTOCView.this.ui); + } + }; + + return helpTOCNavigator; + } + + public HelpModel getHelpModel() { + if (ui == null) { + return null; + } + return ui.getHelpModel(); + } + + @Override + // overrode this method to install our custom factory + public DefaultMutableTreeNode getDataAsTree() { + + DefaultMutableTreeNode superNode = super.getDataAsTree(); + if (superNode.getChildCount() == 0) { + return superNode; // something is not initialized + } + + @SuppressWarnings("rawtypes") + Hashtable viewParameters = getParameters(); + String TOCData = (String) viewParameters.get("data"); + HelpSet helpSet = getHelpSet(); + URL helpSetURL = helpSet.getHelpSetURL(); + URL url; + try { + url = new URL(helpSetURL, TOCData); + } + catch (Exception ex) { + throw new Error("Unable to create tree for view data: " + ex); + } + + return parse(url, helpSet, helpSet.getLocale(), new CustomDefaultTOCFactory(), this); + } + + /** + * Our custom factory that knows how to look for extra XML attributes and how to + * create our custom tree items + */ + public static class CustomDefaultTOCFactory extends DefaultTOCFactory { + @Override + public TreeItem createItem(String tagName, @SuppressWarnings("rawtypes") Hashtable atts, + HelpSet hs, Locale locale) { + + try { + return doCreateItem(tagName, atts, hs, locale); + } + catch (Exception e) { + Msg.error(this, "Unexected error creating a TOC item", e); + throw new RuntimeException("Unexpected error creating a TOC item", e); + } + } + + private TreeItem doCreateItem(String tagName, @SuppressWarnings("rawtypes") Hashtable atts, + HelpSet hs, Locale locale) { + TreeItem item = super.createItem(tagName, atts, hs, locale); + + CustomTreeItemDecorator newItem = new CustomTreeItemDecorator((TOCItem) item); + + if (atts != null) { + String displayText = (String) atts.get("display"); + newItem.setDisplayText(displayText); + String tocID = (String) atts.get("toc_id"); + newItem.setTocID(tocID); + } + return newItem; + } + } + + /** + * Our hook to install our custom cell renderer. + */ + class CustomTOCNavigatorUI extends BasicTOCNavigatorUI { + public CustomTOCNavigatorUI(JHelpTOCNavigator b) { + super(b); + } + + @Override + public void installUI(JComponent c) { + super.installUI(c); + + tree.setExpandsSelectedPaths(true); + } + + @Override + protected void setCellRenderer(NavigatorView view, JTree tree) { + Map map = view.getHelpSet().getCombinedMap(); + tree.setCellRenderer(new CustomCellRenderer(map, (TOCView) view)); + ToolTipManager.sharedInstance().registerComponent(tree); + } + + public HelpModel getHelpModel() { + JHelpNavigator helpNavigator = getHelpNavigator(); + return helpNavigator.getModel(); + } + + // Overridden to change the value used for the 'historyName', which we want to be our + // display name and not the item's name + @Override + public void valueChanged(TreeSelectionEvent e) { + if (isSelectingNodeInternally) { + // ignore our own selection events, as this method will get called twice if we don't + return; + } + + JHelpNavigator navigator = getHelpNavigator(); + HelpModel helpModel = navigator.getModel(); + + TreeItem treeItem = getSelectedItem(e, navigator); + if (treeItem == null) { + return; // nothing selected + } + + TOCItem item = (TOCItem) treeItem; + ID itemID = item.getID(); + if (itemID == null) { + Msg.debug(this, "No help ID for " + item); + return; + } + + String presentation = item.getPresentation(); + if (presentation != null) { + return; // don't currently support presentations + } + + CustomTreeItemDecorator customItem = (CustomTreeItemDecorator) item; + String customDisplayText = customItem.getDisplayText(); + try { + helpModel.setCurrentID(itemID, customDisplayText, navigator); + } + catch (InvalidHelpSetContextException ex) { + Msg.error(this, "Exception setting new help item ID", ex); + } + } + + private TOCItem getSelectedItem(TreeSelectionEvent e, JHelpNavigator navigator) { + TreePath newLeadSelectionPath = e.getNewLeadSelectionPath(); + if (newLeadSelectionPath == null) { + navigator.setSelectedItems(null); + return null; + } + + DefaultMutableTreeNode node = + (DefaultMutableTreeNode) newLeadSelectionPath.getLastPathComponent(); + TOCItem treeItem = (TOCItem) node.getUserObject(); + navigator.setSelectedItems(new TreeItem[] { treeItem }); + + return treeItem; + } + + // Overridden to try to find a parent file for IDs that are based upon anchors within + // a file + @Override + public synchronized void idChanged(HelpModelEvent e) { + selectNodeForID(e.getURL(), e.getID()); + } + + private void selectNodeForID(URL url, ID ID) { + if (ID == null) { + ID = getClosestID(url); + } + + TreePath path = tree.getSelectionPath(); + if (isAlreadySelected(path, ID)) { + return; + } + + DefaultMutableTreeNode node = getNodeForID(topNode, ID); + if (node != null) { + isSelectingNodeInternally = true; + TreePath newPath = new TreePath(node.getPath()); + tree.setSelectionPath(newPath); + tree.scrollPathToVisible(newPath); + isSelectingNodeInternally = false; + return; + } + + // See if the given ID is based upon a URL with an anchor. If that is the case, then + // there may be a node for the parent file of that URL. In that case, select the + // parent file. + if (url == null) { + clearSelection(); + return; + } + + String urlString = url.toExternalForm(); + int anchorIndex = urlString.indexOf('#'); + if (anchorIndex < 0) { + clearSelection(); + return; + } + + urlString = urlString.substring(0, anchorIndex); + try { + URL newURL = new URL(urlString); + selectNodeForID(newURL, null); + } + catch (MalformedURLException e) { + // shouldn't happen, as we are starting with a valid URL + Msg.debug(this, + "Unexpected error create a help URL from an existing URL: " + urlString, e); + } + } + + private ID getClosestID(URL url) { + HelpModel helpModel = toc.getModel(); + HelpSet helpSet = helpModel.getHelpSet(); + Map combinedMap = helpSet.getCombinedMap(); + return combinedMap.getClosestID(url); + } + + private boolean isAlreadySelected(TreePath path, ID id) { + if (path == null) { + return false; + } + + Object pathComponent = path.getLastPathComponent(); + if (!(pathComponent instanceof DefaultMutableTreeNode)) { + return false; + } + + DefaultMutableTreeNode treeNode = (DefaultMutableTreeNode) pathComponent; + TOCItem item = (TOCItem) treeNode.getUserObject(); + if (item == null) { + return false; + } + + ID selectedID = item.getID(); + return selectedID != null && selectedID.equals(id); + } + + private DefaultMutableTreeNode getNodeForID(DefaultMutableTreeNode node, ID ID) { + if (ID == null) { + return null; + } + + if (isNodeID(node, ID)) { + return node; + } + + int childCount = node.getChildCount(); + for (int i = 0; i < childCount; i++) { + DefaultMutableTreeNode matchingNode = + getNodeForID((DefaultMutableTreeNode) node.getChildAt(i), ID); + if (matchingNode != null) { + return matchingNode; + } + } + + return null; + } + + private boolean isNodeID(DefaultMutableTreeNode node, ID ID) { + Object userObject = node.getUserObject(); + if (!(userObject instanceof TOCItem)) { + return false; + } + + TOCItem item = (TOCItem) userObject; + ID nodeID = item.getID(); + if (nodeID == null) { + return false; + } + + return nodeID.equals(ID); + } + + private void clearSelection() { + isSelectingNodeInternally = true; + tree.clearSelection(); + isSelectingNodeInternally = false; + } + } + + static class CustomCellRenderer extends BasicTOCCellRenderer { + + public CustomCellRenderer(Map map, TOCView view) { + super(map, view); + } + + @Override + public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, + boolean expanded, boolean leaf, int row, boolean isFocused) { + + CustomCellRenderer renderer = + (CustomCellRenderer) super.getTreeCellRendererComponent(tree, value, sel, expanded, + leaf, row, isFocused); + + TOCItem item = (TOCItem) ((DefaultMutableTreeNode) value).getUserObject(); + if (item == null) { + return renderer; + } + + CustomTreeItemDecorator customItem = (CustomTreeItemDecorator) item; + renderer.setText(customItem.getDisplayText()); + + if (!SystemUtilities.isInDevelopmentMode()) { + return renderer; + } + + URL url = customItem.getURL(); + if (url != null) { + renderer.setToolTipText(url.toExternalForm()); + return renderer; + } + + ID id = customItem.getID(); + if (id != null) { + renderer.setToolTipText("Missing Help - " + id.id + " in '" + id.hs + "' help set"); + return renderer; + } + + // this can happen if there is no 'target' attribute in the TOC + // (see TOCView.createItem()) + return renderer; + } + } + + /** + * A custom tree item that allows us to store and retrieve custom attributes that we parsed + * from the TOC xml document. + */ + public static class CustomTreeItemDecorator extends javax.help.TOCItem { + + private final TOCItem wrappedItem; + private String displayText; + private String tocID; + private URL cachedURL; + + public CustomTreeItemDecorator(javax.help.TOCItem wrappedItem) { + super(wrappedItem.getID(), wrappedItem.getImageID(), wrappedItem.getHelpSet(), + wrappedItem.getLocale()); + this.wrappedItem = wrappedItem; + } + + void setDisplayText(String text) { + this.displayText = text; + } + + public String getDisplayText() { + return displayText; + } + + void setTocID(String tocID) { + this.tocID = tocID; + } + + public String getTocID() { + return tocID; + } + + @Override + public boolean equals(Object obj) { + return wrappedItem.equals(obj); + } + + @Override + public int getExpansionType() { + return wrappedItem.getExpansionType(); + } + + @Override + public HelpSet getHelpSet() { + return wrappedItem.getHelpSet(); + } + + @Override + public ID getID() { + return wrappedItem.getID(); + } + + @Override + public ID getImageID() { + return wrappedItem.getImageID(); + } + + @Override + public Locale getLocale() { + return wrappedItem.getLocale(); + } + + @Override + public String getMergeType() { + return wrappedItem.getMergeType(); + } + + @Override + public String getName() { + return wrappedItem.getName(); + } + + @Override + public String getPresentation() { + return wrappedItem.getPresentation(); + } + + @Override + public String getPresentationName() { + return wrappedItem.getPresentationName(); + } + + @Override + public URL getURL() { + if (cachedURL == null) { + cachedURL = wrappedItem.getURL(); + } + return cachedURL; + } + + @Override + public int hashCode() { + return wrappedItem.hashCode(); + } + + @Override + public void setExpansionType(int type) { + wrappedItem.setExpansionType(type); + } + + @Override + public void setHelpSet(HelpSet hs) { + wrappedItem.setHelpSet(hs); + } + + @Override + public void setID(ID id) { + wrappedItem.setID(id); + } + + @Override + public void setMergeType(String mergeType) { + wrappedItem.setMergeType(mergeType); + } + + @Override + public void setName(String name) { + wrappedItem.setName(name); + } + + @Override + public void setPresentation(String presentation) { + wrappedItem.setPresentation(presentation); + } + + @Override + public void setPresentationName(String presentationName) { + wrappedItem.setPresentationName(presentationName); + } + + @Override + public String toString() { + return displayText; + } + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/help/GHelpBroker.java b/Ghidra/Framework/Docking/src/main/java/docking/help/GHelpBroker.java new file mode 100644 index 0000000000..9e38bee8bb --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/help/GHelpBroker.java @@ -0,0 +1,720 @@ +/* ### + * 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.help; + +import java.awt.*; +import java.awt.geom.*; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.io.IOException; +import java.net.URL; +import java.util.List; + +import javax.help.*; +import javax.help.event.HelpModelEvent; +import javax.help.event.HelpModelListener; +import javax.swing.*; +import javax.swing.text.*; +import javax.swing.text.html.HTML; +import javax.swing.text.html.HTMLDocument; + +import org.jdesktop.animation.timing.Animator; +import org.jdesktop.animation.timing.TimingTargetAdapter; + +import docking.framework.ApplicationInformationDisplayFactory; +import docking.util.AnimationPainter; +import docking.util.AnimationUtils; +import ghidra.framework.preferences.Preferences; +import ghidra.util.Msg; +import ghidra.util.bean.GGlassPane; +import resources.ResourceManager; + +// NOTE: for JH 2.0, this class has been rewritten to not +// access the 'frame' and 'dialog' variable directly + +/** + * Ghidra help broker that displays the help set; sets the Ghidra icon on + * the help frame and attempts to maintain the user window size. + */ +public class GHelpBroker extends DefaultHelpBroker { + private static final List ICONS = ApplicationInformationDisplayFactory.getWindowIcons(); + private static final int MAX_CALLOUT_RETRIES = 3; + + private Dimension windowSize = new Dimension(1100, 700); + + private boolean initialized; + private JEditorPane htmlEditorPane; + private Animator lastAnimator; + private URL loadingURL; + private PropertyChangeListener pageLoadListener = new PageLoadingListener(); + private HelpModelListener helpModelListener = new HelpIDChangedListener(); + private Window activationWindow; + + // Create the zoom in/out icons that will be added to the default jHelp toolbar. + private static final ImageIcon ZOOM_OUT_ICON = + ResourceManager.loadImage("images/list-remove.png"); + private static final ImageIcon ZOOM_IN_ICON = ResourceManager.loadImage("images/list-add.png"); + + /** + * Construct a new GhidraHelpBroker. + * @param hs java help set associated with this help broker + */ + public GHelpBroker(HelpSet hs) { + super(hs); + } + + @Override + // Overridden so that we can call the preferred version of setCurrentURL on the HelpModel, + // which fixes a bug with the history list (SCR 7639) + public void setCurrentURL(final URL URL) { + + HelpModel model = getHelpModel(); + if (model != null) { + model.setCurrentURL(URL, getHistoryName(URL), null); + } + else { + super.setCurrentURL(URL); + } + } + + /* Perform some shenanigans to force Java Help to reload the given URL */ + void reloadHelpPage(URL url) { + clearContentViewer(); + prepareToCallout(url); + try { + htmlEditorPane.setPage(url); + } + catch (IOException e) { + Msg.error(this, "Unexpected error loading help page: " + url, e); + } + } + + private void clearContentViewer() { + htmlEditorPane.getDocument().putProperty(Document.StreamDescriptionProperty, null); + } + + private JScrollPane getScrollPane(JEditorPane editorPane) { + Container parent = editorPane.getParent(); + while (parent != null) { + if (parent instanceof JScrollPane) { + return (JScrollPane) parent; + } + parent = parent.getParent(); + } + return null; + } + + private JEditorPane getHTMLEditorPane(JHelpContentViewer contentViewer) { + // + // Intimate Knowledge - construction of the viewer: + // + // -BorderLayout + // -JScrollPane + // -Viewport + // -JHEditorPane extends JEditorPane + // + // + Component[] components = contentViewer.getComponents(); + JScrollPane scrollPane = (JScrollPane) components[0]; + JViewport viewport = scrollPane.getViewport(); + + return (JEditorPane) viewport.getView(); + } + + private HelpModel getHelpModel() { + // + // Unusual Code Alert!: We have opened up access to the help system's HelpModel by way + // of our CustomTOCView object that we install elsewhere. We need + // access to the model because of a bug in the help system + // (SCR 7639). Unfortunately, the Java Help system does not give us + // access to the model directly, but we have opened up the access from + // one of our overriding components. The following code is + // digging-out our custom component to get at the model. An + // alternative approach would be to just use reflection and violate + // security restrictions, but that seemed worse than this solution. + // + + WindowPresentation windowPresentation = getWindowPresentation(); + HelpSet helpSet = windowPresentation.getHelpSet(); + NavigatorView tocView = helpSet.getNavigatorView("TOC"); + if (!(tocView instanceof CustomTOCView)) { + // not sure how this could happen + Msg.debug(this, "The help system is not using the CustomTOCView class!"); + return null; + } + + CustomTOCView customTOCView = (CustomTOCView) tocView; + return customTOCView.getHelpModel(); + } + + @Override + public void setDisplayed(boolean b) { + if (!b) { + super.setDisplayed(b); + return; + } + + // this must be before any call that triggers the help system to create its window + initializeScreenDevice(); + + WindowPresentation windowPresentation = getWindowPresentation(); + updateWindowSize(windowPresentation); + + // this has to be before getHelpWindow() or the value returned will be null + super.setDisplayed(b); + + initializeUIWindowPresentation(windowPresentation); + } + + private void initializeScreenDevice() { + if (initialized) { + return; + } + + if (activationWindow == null) { + // This can happen when we show the 'What's New' help page on a fresh install. In + // that case, we were not activated from an existing window, thus, there may + // be no parent window. + return; + } + + GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); + GraphicsDevice[] gs = ge.getScreenDevices(); + GraphicsConfiguration config = activationWindow.getGraphicsConfiguration(); + GraphicsDevice parentDevice = config.getDevice(); + for (int i = 0; i < gs.length; i++) { + if (gs[i] == parentDevice) { + // update the help window's screen to match that of the parent + setScreen(i); + } + } + } + + private void initializeUIWindowPresentation(WindowPresentation windowPresentation) { + + Window helpWindow = windowPresentation.getHelpWindow(); + Container contentPane = null; + if (helpWindow instanceof JFrame) { + JFrame frame = (JFrame) helpWindow; + installRootPane(frame); + frame.setIconImages(ICONS); + contentPane = frame.getContentPane(); + } + else if (helpWindow instanceof JDialog) { + JDialog dialog = (JDialog) helpWindow; + installRootPane(dialog); + contentPane = dialog.getContentPane(); + } + + initializeUIComponents(contentPane); + } + + private void initializeUIComponents(Container contentPane) { + + // the editor pane can be changed out from under us, such as when the UI is updated when + // the theme changes + Component[] components = contentPane.getComponents(); + JHelp jHelp = (JHelp) components[0]; + JHelpContentViewer contentViewer = jHelp.getContentViewer(); + JEditorPane activeHtmlPane = getHTMLEditorPane(contentViewer); + if (activeHtmlPane == htmlEditorPane && initialized) { + return; // already initialized + } + + addCustomToolbarItems(jHelp); + htmlEditorPane = getHTMLEditorPane(contentViewer); + + // just creating the search wires everything together + HelpModel helpModel = getHelpModel(); + helpModel.addHelpModelListener(helpModelListener); + new HelpViewSearcher(jHelp, helpModel); + + installActions(jHelp); + } + + void reload() { + clearHighlights(); + initialized = false; + if (isDisplayed()) { + setDisplayed(false); + setDisplayed(true); + } + } + + private void clearHighlights() { + TextHelpModel helpModel = (TextHelpModel) getHelpModel(); + if (helpModel != null) { + helpModel.removeAllHighlights(); + } + } + + /** + * Create zoom in/out buttons on the default help window toolbar. + * @param jHelp the java help object used to retrieve the help components + */ + protected void addCustomToolbarItems(final JHelp jHelp) { + + for (Component component : jHelp.getComponents()) { + if (component instanceof JToolBar) { + JToolBar toolbar = (JToolBar) component; + toolbar.addSeparator(); + + ImageIcon zoomOutIcon = ResourceManager.getScaledIcon(ZOOM_OUT_ICON, 24, 24); + JButton zoomOutBtn = new JButton(zoomOutIcon); + zoomOutBtn.setToolTipText("Zoom out"); + zoomOutBtn.addActionListener(e -> { + GHelpHTMLEditorKit.zoomOut(); + + // Need to reload the page to force the scroll panes to resize properly. A + // simple revalidate/repaint won't do it. + reloadHelpPage(getCurrentURL()); + }); + toolbar.add(zoomOutBtn); + + ImageIcon zoomInIcon = ResourceManager.getScaledIcon(ZOOM_IN_ICON, 24, 24); + JButton zoomInBtn = new JButton(zoomInIcon); + zoomInBtn.setToolTipText("Zoom in"); + zoomInBtn.addActionListener(e -> { + GHelpHTMLEditorKit.zoomIn(); + + // Need to reload the page to force the scroll panes to resize properly. A + // simple revalidate/repaint won't do it. + reloadHelpPage(getCurrentURL()); + }); + toolbar.add(zoomInBtn); + + // Once we've found the toolbar we can break out of the loop and stop looking for it. + break; + } + } + } + + private void installActions(JHelp help) { + JToolBar toolbar = null; + Component[] components = help.getComponents(); + for (Component c : components) { + if (c instanceof JToolBar) { + toolbar = (JToolBar) c; + break; + } + } + + if (toolbar == null) { + // shouldn't happen + return; + } + + // separate the Java help stuff from our actions + toolbar.addSeparator(); + + ToggleNavigationAid action = new ToggleNavigationAid(); + toolbar.add(new JButton(action)); + } + + private String getHistoryName(URL URL) { + String text = URL.getFile(); + int index = text.lastIndexOf('/'); + if (index != -1) { + // we want just the filename + text = text.substring(index + 1); + } + + String ref = URL.getRef(); + if (ref != null) { + text += " - " + ref; + } + return text; + } + + private void installRootPane(JFrame frame) { + Component oldGlassPane = frame.getGlassPane(); + if (!(oldGlassPane instanceof GGlassPane)) { + GGlassPane gGlassPane = new GGlassPane(); + frame.setGlassPane(gGlassPane); + gGlassPane.setVisible(true); + } + } + + private void installRootPane(JDialog dialog) { + Component oldGlassPane = dialog.getGlassPane(); + if (!(oldGlassPane instanceof GGlassPane)) { + GGlassPane gGlassPane = new GGlassPane(); + dialog.setGlassPane(gGlassPane); + gGlassPane.setVisible(true); + } + } + + private void updateWindowSize(WindowPresentation presentation) { + if (windowSize == null) { + return; + } + + presentation.createHelpWindow(); + presentation.setSize(windowSize); + } + + @Override + public void setActivationWindow(Window window) { + WindowPresentation windowPresentation = getWindowPresentation(); + Window helpWindow = windowPresentation.getHelpWindow(); + if (helpWindow == null) { + activationWindow = window; + super.setActivationWindow(window); + return; + } + + windowSize = helpWindow.getSize();// remember the previous size + + boolean wasModal = isModalWindow(helpWindow); + boolean willBeModal = isModalWindow(window); + if (!wasModal && willBeModal) { + // in this condition, a new window will be shown, but the old one is not properly + // closed by JavaHelp + helpWindow.setVisible(false); + } + + super.setActivationWindow(window); + } + + private boolean isModalWindow(Window window) { + if (window instanceof Dialog) { + Dialog dialog = (Dialog) window; + if (dialog.isModal()) { + return true; + } + } + return false; + } + + private void showNavigationAid() { + String showAidString = Preferences.getProperty(HelpManager.SHOW_AID_KEY); + if (showAidString == null) { + return; + } + + boolean showAid = Boolean.parseBoolean(showAidString); + if (!showAid) { + return; + } + + calloutReferenceLater(); + } + + private void calloutReferenceLater() { + SwingUtilities.invokeLater(() -> calloutReference(loadingURL)); + } + + private void calloutReference(final URL url) { + String ref = url.getRef(); + if (ref == null) { + return; + } + + final Rectangle area = getReferenceArea(ref); + if (area == null) { + return; + } + + doCalloutReference(area, 0); + } + + private Rectangle getReferenceArea(String ref) { + HTMLDocument document = (HTMLDocument) htmlEditorPane.getDocument(); + HTMLDocument.Iterator iter = document.getIterator(HTML.Tag.A); + for (; iter.isValid(); iter.next()) { + AttributeSet attributes = iter.getAttributes(); + String name = (String) attributes.getAttribute(HTML.Attribute.NAME); + if (name == null || !name.equals(ref)) { + continue; + } + + try { + int start = iter.getStartOffset(); + Rectangle2D startArea = htmlEditorPane.modelToView2D(start); + return startArea.getBounds(); + } + catch (BadLocationException ble) { + Msg.trace(this, "Unexpected exception searching for help reference", ble); + } + } + return null; + } + + /** + * This method exists to address the threaded timing nature of how the help system loads + * help pages and when the UI is adjusted in response to those changes. + *

+ * Note: this method will call itself if the view is not yet updated for the requested + * model change. In that case, this method will call itself again later. This may + * need to happen more than once. However, we will only try a few times and + * then just give up. + * + * @param area the area to call out + * @param callCount the number number of times this method has already been called + */ + private void doCalloutReference(final Rectangle area, int callCount) { + + if (callCount > MAX_CALLOUT_RETRIES) { + // this probably can't happen, but we don't want to keep calling this method + // forever. + return; + } + + WindowPresentation windowPresentation = getWindowPresentation(); + Window helpWindow = windowPresentation.getHelpWindow(); + Container contentPane = null; + if (helpWindow instanceof JDialog) { + contentPane = ((JDialog) helpWindow).getContentPane(); + } + else { + contentPane = ((JFrame) helpWindow).getContentPane(); + } + + JScrollPane scrollPane = getScrollPane(htmlEditorPane); + JViewport viewport = scrollPane.getViewport(); + Point viewPosition = viewport.getViewPosition(); + + final int numberOfCalls = callCount + 1; + if (viewPosition.x == 0 && viewPosition.y == 0) { + + // + // Unusual Code: Not yet rendered! Try again. + // + SwingUtilities.invokeLater(() -> doCalloutReference(area, numberOfCalls)); + + return; + } + + // + // The area of the HTML content is absolute inside of the entire document. + // However, the user is viewing the document inside of a scroll pane. So, we + // want the offset of the element within the viewer, not the absolute position. + // + area.y -= viewPosition.y; + + // + // Update the coordinates to be relative to the content pane, which is where we + // are doing the painting. + // + Rectangle relativeArea = SwingUtilities.convertRectangle(scrollPane, area, contentPane); + Shape star = new StarShape(relativeArea.getLocation()); + + Animator animator = + AnimationUtils.createPaintingAnimator(helpWindow, new LocationHintPainter(star)); + if (animator == null) { + return; + } + + lastAnimator = animator; + lastAnimator.addTarget(new TimingTargetAdapter() { + @Override + public void end() { + lastAnimator = null; + } + }); + } + + private void prepareToCallout(URL url) { + if (lastAnimator != null) { + // prevent animations from lingering when moving to new pages + lastAnimator.stop(); + } + + loadingURL = url; + + // TODO + // updateTitle(); + + if (isCurrentPage(loadingURL)) { + showNavigationAid(); + return;// page already loaded; no need to use the listener + } + + // listen for the page to be loaded, as it is asynchronous + htmlEditorPane.removePropertyChangeListener("page", pageLoadListener); + htmlEditorPane.addPropertyChangeListener("page", pageLoadListener); + } + + private boolean isCurrentPage(URL newURL) { + if (newURL == null) { + return false;// not sure if this can happen + } + + String newFile = newURL.getFile(); + URL currentURL = htmlEditorPane.getPage(); + if (currentURL == null) { + return false; + } + + String currentFile = currentURL.getFile(); + return newFile.equals(currentFile); + } + +//================================================================================================== +// Inner Classes +//================================================================================================== + + private class StarShape extends Path2D.Float { + + StarShape(Point location) { + this(5, location, 1, .3);// reasonable star qualities + } + + StarShape(int points, Point location, double outerRadius, double innerRadius) { + // note: location is the origin of the shape + + double angle = Math.PI / points; + GeneralPath path = new GeneralPath(); + + int scale = 20; + double lr = Math.max(outerRadius, innerRadius); + int width = (int) (scale * (2 * lr)); + int height = width;// square bounds + double cx = location.x + width / 2; + double cy = location.y + height / 2; + Point2D.Double center = new Point2D.Double(cx, cy); + + // start the first point... + double r = outerRadius; + double x = center.x + Math.cos(0 * angle) * r; + double y = center.y + Math.sin(0 * angle) * r; + path.moveTo(x, y); + + // ...the remaining points + for (int i = 1; i < 2 * points; i++) { + r = (i % 2) == 0 ? outerRadius : innerRadius; + x = center.x + Math.cos(i * angle) * r; + y = center.y + Math.sin(i * angle) * r; + path.lineTo(x, y); + } + + path.closePath(); + + // scaled center x/y + double scx = scale * cx; + double scy = scale * cy; + + // note: An offset of (width / 2) moves from center to 0. This didn't look quite + // right, so, through trial and error, we updated the offset so that the + // shape's location is just over the beginning of the text that follows the + // anchor, in most cases. + double offsetx = width / 4; + double offsety = height / 4; + + // scaled offset x/y + double sox = scx - offsetx;// move the x from center to 0 + double soy = scy - offsety;// ... + + // delta x/y + double dx = sox - location.x; + double dy = soy - location.y; + + // move the origin so that after we scale, it goes back to 0,0 + AffineTransform xform = AffineTransform.getTranslateInstance(-dx, -dy); + xform.scale(scale, scale); + + Shape shape = xform.createTransformedShape(path); + super.append(shape, true); + } + } + + private class PageLoadingListener implements PropertyChangeListener { + @Override + public void propertyChange(PropertyChangeEvent evt) { + showNavigationAid(); + htmlEditorPane.removePropertyChangeListener("page", pageLoadListener); + } + } + + private class HelpIDChangedListener implements HelpModelListener { + @Override + public void idChanged(HelpModelEvent e) { + prepareToCallout(e.getURL()); + } + } + + private class LocationHintPainter implements AnimationPainter { + + private Color color = new Color(100, 100, 255, 100); + private Shape paintShape; + + LocationHintPainter(Shape paintShape) { + this.paintShape = paintShape; + } + + @Override + public void paint(GGlassPane glassPane, Graphics graphics, double percentComplete) { + + Graphics2D g2d = (Graphics2D) graphics; + g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR); + + // + // At 0%, with 100% opacity; at the end, paint with 0% opacity + // + Composite originalComposite = g2d.getComposite(); + AlphaComposite alphaComposite = AlphaComposite.getInstance( + AlphaComposite.SrcOver.getRule(), (float) (1 - percentComplete)); + g2d.setComposite(alphaComposite); + + double transition = 1 - percentComplete; + Color originalColor = g2d.getColor(); + + AffineTransform originalTransform = g2d.getTransform(); + + double scale = 4 * transition; + + int degrees = (int) (480 * transition); + double rad = Math.toRadians(transition * degrees); + + Rectangle b = paintShape.getBounds(); + double cx = b.getCenterX(); + double cy = b.getCenterY(); + double scx = cx * scale; + double scy = cy * scale; + double dcx = scx - cx; + double dcy = scy - cy; + + AffineTransform scaler = new AffineTransform(); + scaler.translate(-dcx, -dcy); + scaler.scale(scale, scale); + Shape scaled = scaler.createTransformedShape(paintShape); + + AffineTransform rotater = new AffineTransform(); + rotater.rotate(rad, cx, cy); + Shape finalShape = rotater.createTransformedShape(scaled); + + /* + // Debug + Shape box = scaler.createTransformedShape(b); + g2d.setColor(Color.GREEN); + g2d.fill(box); + + box = transform.createTransformedShape(box); + g2d.setColor(Color.YELLOW); + g2d.fill(box); + */ + + g2d.setColor(color); + g2d.fill(finalShape); + + g2d.setColor(originalColor); + g2d.setTransform(originalTransform); + g2d.setComposite(originalComposite); + } + + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/help/HelpManager.java b/Ghidra/Framework/Docking/src/main/java/docking/help/HelpManager.java index a1f47a5a98..8816a1da01 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/help/HelpManager.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/help/HelpManager.java @@ -28,11 +28,9 @@ import java.util.Map.Entry; import javax.help.*; import javax.help.Map.ID; import javax.swing.JButton; -import javax.swing.UIManager; import docking.ComponentProvider; import docking.action.DockingActionIf; -import docking.theme.GColor; import generic.concurrent.GThreadPool; import generic.util.WindowUtilities; import ghidra.util.*; @@ -89,8 +87,6 @@ public class HelpManager implements HelpService { mainHB = mainHS.createHelpBroker(); mainHS.setTitle(GHIDRA_HELP_TITLE); - setColorResources(); - isValidHelp = isValidHelp(); } @@ -196,6 +192,18 @@ public class HelpManager implements HelpService { return mainHS; } + @Override + public void reload() { + + if (!(mainHB instanceof GHelpBroker)) { + // not our broker installed; can't force a reload + return; + } + + GHelpBroker gHelpBroker = (GHelpBroker) mainHB; + gHelpBroker.reload(); + } + @Override public void showHelp(URL url) { if (!isValidHelp) { @@ -693,15 +701,6 @@ public class HelpManager implements HelpService { return null; } - /** - * Set the color resources on the JEditorPane for selection so that - * you can see the highlights when you do a search in the JavaHelp. - */ - private void setColorResources() { - UIManager.put("EditorPane.selectionBackground", new GColor("color.bg.selection.help")); - UIManager.put("EditorPane.selectionForeground", UIManager.get("EditorPane.foreground")); - } - private void displayHelpInfo(Object helpObj, HelpLocation loc, Window parent) { String msg = getHelpInfo(helpObj, loc); Msg.showInfo(this, parent, "Help Info", msg); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/help/HelpService.java b/Ghidra/Framework/Docking/src/main/java/docking/help/HelpService.java new file mode 100644 index 0000000000..699d12a995 --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/help/HelpService.java @@ -0,0 +1,121 @@ +/* ### + * 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.help; + +import java.awt.Component; +import java.net.URL; + +import ghidra.util.HelpLocation; + +/** + * HelpService defines a service for displaying Help content by an ID or URL. + */ +public interface HelpService { + + public static final String DUMMY_HELP_SET_NAME = "Dummy_HelpSet.hs"; + + /** + * Display the Help content identified by the help object. + * + * @param helpObject the object to which help was previously registered + * @param infoOnly display {@link HelpLocation} information only, not the help UI + * @param parent requesting component + * + * @see #registerHelp(Object, HelpLocation) + */ + public void showHelp(Object helpObject, boolean infoOnly, Component parent); + + /** + * Display the help page for the given URL. This is a specialty method for displaying + * help when a specific file is desired, like an introduction page. Showing help for + * objects within the system is accomplished by calling + * {@link #showHelp(Object, boolean, Component)}. + * + * @param url the URL to display + * @see #showHelp(Object, boolean, Component) + */ + public void showHelp(URL url); + + /** + * Display the help page for the given help location. + * + * @param location the location to display. + * @see #showHelp(Object, boolean, Component) + */ + public void showHelp(HelpLocation location); + + /** + * Signals to the help system to ignore the given object when searching for and validating + * help. Once this method has been called, no help can be registered for the given object. + * + * @param helpObject the object to exclude from the help system. + */ + public void excludeFromHelp(Object helpObject); + + /** + * Returns true if the given object is meant to be ignored by the help system + * + * @param helpObject the object to check + * @return true if ignored + * @see #excludeFromHelp(Object) + */ + public boolean isExcludedFromHelp(Object helpObject); + + /** + * Register help for a specific object. + * + *

Do not call this method will a null help location. Instead, to signal that + * an item has no help, call {@link #excludeFromHelp(Object)}. + * + * @param helpObject the object to associate the specified help location with + * @param helpLocation help content location + */ + public void registerHelp(Object helpObject, HelpLocation helpLocation); + + /** + * Removes this object from the help system. This method is useful, for example, + * when a single Java {@link Component} will have different help locations + * assigned over its lifecycle. + * + * @param helpObject the object for which to clear help + */ + public void clearHelp(Object helpObject); + + /** + * Returns the registered (via {@link #registerHelp(Object, HelpLocation)} help + * location for the given object; null if there is no registered + * help. + * + * @param object The object for which to find a registered HelpLocation. + * @return the registered HelpLocation + * @see #registerHelp(Object, HelpLocation) + */ + public HelpLocation getHelpLocation(Object object); + + /** + * Returns true if the help system has been initialized properly; false if help does not + * exist or is not working. + * + * @return true if the help system has found the applications help content and has finished + * initializing + */ + public boolean helpExists(); + + /** + * Called when a major system even happens, such as changing the system theme. + */ + public void reload(); +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/DefaultTheme.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/DefaultTheme.java index fb1d4e5cd4..421b360189 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/DefaultTheme.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/DefaultTheme.java @@ -18,6 +18,6 @@ package docking.theme; public class DefaultTheme extends DiscoverableGTheme { public DefaultTheme() { - super("Default", LookAndFeelType.SYSTEM); + super("Default", LafType.SYSTEM); } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/DiscoverableGTheme.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/DiscoverableGTheme.java index 22084aa40b..b8a06ed4c1 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/DiscoverableGTheme.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/DiscoverableGTheme.java @@ -20,12 +20,8 @@ import ghidra.util.classfinder.ExtensionPoint; public abstract class DiscoverableGTheme extends GTheme implements ExtensionPoint { static final String CLASS_PREFIX = "Class:"; - protected DiscoverableGTheme(String name, LookAndFeelType lookAndFeel) { - super(name, lookAndFeel, false); - } - - protected DiscoverableGTheme(String name, LookAndFeelType lookAndFeel, boolean isDark) { - super(name, lookAndFeel, isDark); + protected DiscoverableGTheme(String name, LafType lookAndFeel) { + super(name, lookAndFeel); } @Override diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/FileGTheme.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/FileGTheme.java index c3a7064aef..41ee127a16 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/FileGTheme.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/FileGTheme.java @@ -15,8 +15,13 @@ */ package docking.theme; -import java.io.File; -import java.io.IOException; +import java.awt.Color; +import java.awt.Font; +import java.io.*; +import java.util.Collections; +import java.util.List; + +import ghidra.util.WebColors; public class FileGTheme extends GTheme { public static final String FILE_PREFIX = "File:"; @@ -26,8 +31,13 @@ public class FileGTheme extends GTheme { this(file, new ThemeReader(file)); } + public FileGTheme(File file, String name, LafType laf) { + super(name, laf); + this.file = file; + } + FileGTheme(File file, ThemeReader reader) { - super(reader.getThemeName(), reader.getLookAndFeelType(), reader.isDark()); + super(reader.getThemeName(), reader.getLookAndFeelType()); this.file = file; reader.loadValues(this); } @@ -37,4 +47,93 @@ public class FileGTheme extends GTheme { return FILE_PREFIX + file.getAbsolutePath(); } + public boolean canSave() { + return file.canWrite(); + } + + public File getFile() { + return file; + } + + public void save() throws IOException { + try (BufferedWriter writer = new BufferedWriter(new FileWriter(file))) { + List colors = getColors(); + Collections.sort(colors); + + List fonts = getFonts(); + Collections.sort(fonts); + + List icons = getIcons(); + Collections.sort(icons); + + writer.write(THEME_NAME_KEY + " = " + getName()); + writer.newLine(); + + writer.write(THEME_LOOK_AND_FEEL_KEY + " = " + getLookAndFeelType().getName()); + writer.newLine(); + + for (ColorValue colorValue : colors) { + String outputId = colorValue.toExternalId(colorValue.getId()); + writer.write(outputId + " = " + getValueOutput(colorValue)); + writer.newLine(); + } + + for (FontValue fontValue : fonts) { + String outputId = fontValue.toExternalId(fontValue.getId()); + writer.write(outputId + " = " + getValueOutput(fontValue)); + writer.newLine(); + } + + for (IconValue iconValue : icons) { + String outputId = iconValue.toExternalId(iconValue.getId()); + writer.write(outputId + " = " + getValueOutput(iconValue)); + writer.newLine(); + } + + } + + } + + private String getValueOutput(ColorValue colorValue) { + if (colorValue.getReferenceId() != null) { + return colorValue.toExternalId(colorValue.getReferenceId()); + } + Color color = colorValue.getRawValue(); + String outputString = WebColors.toString(color, false); + String colorName = WebColors.toWebColorName(color); + if (colorName != null) { + outputString += " // " + colorName; + } + return outputString; + } + + private String getValueOutput(IconValue iconValue) { + if (iconValue.getReferenceId() != null) { + return iconValue.toExternalId(iconValue.getReferenceId()); + } + return iconValue.getRawValue(); + } + + private String getValueOutput(FontValue fontValue) { + if (fontValue.getReferenceId() != null) { + return fontValue.toExternalId(fontValue.getReferenceId()); + } + Font font = fontValue.getRawValue(); + return String.format("%s-%s-%s", font.getName(), getStyleString(font), font.getSize()); + } + + private String getStyleString(Font font) { + boolean bold = font.isBold(); + boolean italic = font.isItalic(); + if (bold && italic) { + return "BOLDITALIC"; + } + if (bold) { + return "BOLD"; + } + if (italic) { + return "ITALIC"; + } + return "PLAIN"; + } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/GTheme.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/GTheme.java index d1f28cc2ea..193267e73d 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/GTheme.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/GTheme.java @@ -17,26 +17,25 @@ package docking.theme; import java.awt.Color; import java.awt.Font; -import java.io.*; -import java.util.*; - -import ghidra.util.WebColors; +import java.io.File; +import java.io.IOException; +import java.util.Objects; /** * Class to store all the configurable appearance properties (Colors, Fonts, Icons, Look and Feel) * in an application. */ public class GTheme extends GThemeValueMap { + public static String FILE_EXTENSION = ".theme"; + static final String THEME_NAME_KEY = "name"; static final String THEME_LOOK_AND_FEEL_KEY = "lookAndFeel"; - static final String THEME_IS_DARK_KEY = "dark"; private final String name; - private final LookAndFeelType lookAndFeel; - private final boolean isDark; + private final LafType lookAndFeel; public GTheme(String name) { - this(name, LookAndFeelType.SYSTEM, false); + this(name, LafType.SYSTEM); } @@ -44,13 +43,10 @@ public class GTheme extends GThemeValueMap { * Creates a new empty GTheme with the given name * @param name the name for the new GTheme * @param lookAndFeel the look and feel type used by this theme - * @param isDark true if this theme uses dark backgrounds instead of the standard - * light backgrounds */ - protected GTheme(String name, LookAndFeelType lookAndFeel, boolean isDark) { + protected GTheme(String name, LafType lookAndFeel) { this.name = name; this.lookAndFeel = lookAndFeel; - this.isDark = isDark; } /** @@ -65,7 +61,7 @@ public class GTheme extends GThemeValueMap { * Returns the name of the LookAndFeel associated with this GTheme * @return the name of the LookAndFeel associated with this GTheme */ - public LookAndFeelType getLookAndFeelType() { + public LafType getLookAndFeelType() { return lookAndFeel; } @@ -74,7 +70,7 @@ public class GTheme extends GThemeValueMap { * @return true if this theme should use dark defaults */ public boolean isDark() { - return isDark; + return lookAndFeel.isDark(); } /** @@ -161,123 +157,21 @@ public class GTheme extends GThemeValueMap { return false; } GTheme other = (GTheme) obj; - return Objects.equals(name, other.name) && Objects.equals(lookAndFeel, other.lookAndFeel) && - Objects.equals(isDark, other.isDark); - } - - /** - * Creates a new file based GTheme with the same values as this GTheme - * @param saveToFile file to associate and save this GTheme to - * @return the new theme - * @throws IOException if a general I/O exception occurs - */ - public GTheme saveToFile(File saveToFile) throws IOException { - return doSaveToFile(saveToFile, this); - } - - /** - * Creates a new file based GTheme with the same values as this GTheme and includes default - * values not modified by this theme. - * @param saveToFile file to associate and save this GTheme to - * @param defaults the collection of default values to include in the output file - * @return the new theme - * @throws IOException if a general I/O exception occurs - */ - public GTheme saveToFile(File saveToFile, GThemeValueMap defaults) throws IOException { - GThemeValueMap combined = new GThemeValueMap(); - combined.load(defaults); - combined.load(this); - return doSaveToFile(saveToFile, combined); - } - - private GTheme doSaveToFile(File saveToFile, GThemeValueMap values) throws IOException { - try (BufferedWriter writer = new BufferedWriter(new FileWriter(saveToFile))) { - List colors = values.getColors(); - Collections.sort(colors); - - List fonts = values.getFonts(); - Collections.sort(fonts); - - List icons = values.getIcons(); - Collections.sort(icons); - - writer.write(THEME_NAME_KEY + " = " + name); - writer.newLine(); - - writer.write(THEME_LOOK_AND_FEEL_KEY + " = " + lookAndFeel.getName()); - writer.newLine(); - - if (isDark()) { - writer.write(THEME_IS_DARK_KEY + " = true"); - writer.newLine(); - } - - for (ColorValue colorValue : colors) { - String outputId = colorValue.toExternalId(colorValue.getId()); - writer.write(outputId + " = " + getValueOutput(colorValue)); - writer.newLine(); - } - - for (FontValue fontValue : fonts) { - String outputId = fontValue.toExternalId(fontValue.getId()); - writer.write(outputId + " = " + getValueOutput(fontValue)); - writer.newLine(); - } - - for (IconValue iconValue : icons) { - String outputId = iconValue.toExternalId(iconValue.getId()); - writer.write(outputId + " = " + getValueOutput(iconValue)); - writer.newLine(); - } - - } - return new FileGTheme(saveToFile); - } - - private String getValueOutput(IconValue iconValue) { - if (iconValue.getReferenceId() != null) { - return iconValue.toExternalId(iconValue.getReferenceId()); - } - return iconValue.getRawValue(); - } - - private String getValueOutput(FontValue fontValue) { - if (fontValue.getReferenceId() != null) { - return fontValue.toExternalId(fontValue.getReferenceId()); - } - Font font = fontValue.getRawValue(); - return String.format("%s-%s-%s", font.getName(), getStyleString(font), font.getSize()); - } - - private String getStyleString(Font font) { - boolean bold = font.isBold(); - boolean italic = font.isItalic(); - if (bold && italic) { - return "BOLDITALIC"; - } - if (bold) { - return "BOLD"; - } - if (italic) { - return "ITALIC"; - } - return "PLAIN"; - } - - private String getValueOutput(ColorValue colorValue) { - if (colorValue.getReferenceId() != null) { - return colorValue.toExternalId(colorValue.getReferenceId()); - } - Color color = colorValue.getRawValue(); - String outputString = WebColors.toString(color, false); - String colorName = WebColors.toWebColorName(color); - if (colorName != null) { - outputString += " // " + colorName; - } - return outputString; + return Objects.equals(name, other.name) && Objects.equals(lookAndFeel, other.lookAndFeel); } public boolean hasSupportedLookAndFeel() { return lookAndFeel.isSupported(); } + + public FileGTheme saveToFile(File file, boolean includeDefaults) throws IOException { + FileGTheme fileTheme = new FileGTheme(file, name, lookAndFeel); + if (includeDefaults) { + fileTheme.load(Gui.getDefaults()); + } + fileTheme.load(this); + fileTheme.save(); + return fileTheme; + } + } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/GThemeDefaults.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/GThemeDefaults.java index 25e94b0ada..b03fd5b740 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/GThemeDefaults.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/GThemeDefaults.java @@ -65,6 +65,7 @@ public class GThemeDefaults { public static final GColor GOLD = new GColor("color.palette.gold"); public static final GColor GRAY = new GColor("color.palette.gray"); public static final GColor GREEN = new GColor("color.palette.green"); + public static final GColor LIGHT_GRAY = new GColor("color.palette.lightgray"); public static final GColor LIME = new GColor("color.palette.lime"); public static final GColor ORANGE = new GColor("color.palette.orange"); public static final GColor PINK = new GColor("color.palette.pink"); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/GThemeValueMap.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/GThemeValueMap.java index b6dfca0982..297570b4f2 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/GThemeValueMap.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/GThemeValueMap.java @@ -99,4 +99,32 @@ public class GThemeValueMap { fontMap.clear(); iconMap.clear(); } + + public boolean isEmpty() { + return colorMap.isEmpty() && fontMap.isEmpty() && iconMap.isEmpty(); + } + + public void removeColor(String id) { + colorMap.remove(id); + } + + public GThemeValueMap removeSameValues(GThemeValueMap defaults) { + GThemeValueMap map = new GThemeValueMap(); + for (ColorValue color : colorMap.values()) { + if (!color.equals(defaults.getColor(color.getId()))) { + map.addColor(color); + } + } + for (FontValue font : fontMap.values()) { + if (!font.equals(defaults.getFont(font.getId()))) { + map.addFont(font); + } + } + for (IconValue icon : iconMap.values()) { + if (!icon.equals(defaults.getIcon(icon.getId()))) { + map.addIconPath(icon); + } + } + return map; + } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/Gui.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/Gui.java index bcd1b461e0..86df260273 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/Gui.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/Gui.java @@ -23,7 +23,11 @@ import java.util.List; import javax.swing.*; import javax.swing.plaf.UIResource; +import com.formdev.flatlaf.*; + import docking.framework.ApplicationInformationDisplayFactory; +import docking.help.Help; +import docking.theme.builtin.JavaColorMapping; import ghidra.framework.Application; import ghidra.framework.preferences.Preferences; import ghidra.util.Msg; @@ -41,13 +45,11 @@ public class Gui { private static GTheme activeTheme = new DefaultTheme(); private static Set allThemes; - private static GThemeValueMap ghidraCoreDefaults = new GThemeValueMap(); - private static GThemeValueMap originalJavaDefaults; -// private static GThemeValueMap convertedJavaDefaults; + private static GThemeValueMap ghidraLightDefaults = new GThemeValueMap(); + private static GThemeValueMap ghidraDarkDefaults = new GThemeValueMap(); + private static GThemeValueMap javaDefaults = new GThemeValueMap(); private static GThemeValueMap currentValues = new GThemeValueMap(); - private static GThemeValueMap darkDefaults = new GThemeValueMap(); - private static ThemePropertiesLoader themePropertiesLoader = new ThemePropertiesLoader(); private static Map gColorMap = new HashMap<>(); @@ -63,91 +65,75 @@ public class Gui { } public static void initialize() { - loadThemeDefaults(); + installFlatLookAndFeels(); + loadGhidraDefaults(); setTheme(getThemeFromPreferences()); // LookAndFeelUtils.installGlobalOverrides(); platformSpecificFixups(); } - private static void loadThemeDefaults() { - themePropertiesLoader.load(); - ghidraCoreDefaults = themePropertiesLoader.getDefaults(); - darkDefaults = themePropertiesLoader.getDarkDefaults(); + private static void installFlatLookAndFeels() { + UIManager.installLookAndFeel(LafType.FLAT_LIGHT.getName(), FlatLightLaf.class.getName()); + UIManager.installLookAndFeel(LafType.FLAT_DARK.getName(), FlatDarkLaf.class.getName()); + UIManager.installLookAndFeel(LafType.FLAT_DARCULA.getName(), + FlatDarculaLaf.class.getName()); } - public static void reloadThemeDefaults() { - loadThemeDefaults(); - currentValues = buildCurrentValues(activeTheme); - refresh(); + private static void loadGhidraDefaults() { + themePropertiesLoader.load(); + ghidraLightDefaults = themePropertiesLoader.getDefaults(); + ghidraDarkDefaults = themePropertiesLoader.getDarkDefaults(); + } + + public static void reloadGhidraDefaults() { + loadGhidraDefaults(); + buildCurrentValues(); + } + + public static void restoreThemeValues() { + buildCurrentValues(); } public static void setTheme(GTheme theme) { if (theme.hasSupportedLookAndFeel()) { activeTheme = theme; - LookAndFeelType lookAndFeel = theme.getLookAndFeelType(); + LafType lookAndFeel = theme.getLookAndFeelType(); try { lookAndFeel.install(); + saveThemeToPreferences(theme); + fixupJavaDefaults(); + // The help may produce errors when switching the theme, such as if there is an + // active search in the help. We have added this call to allow the help system + // to cleanup some internal state. + Help.getHelpService().reload(); + buildCurrentValues(); + updateUIs(); } catch (Exception e) { - Msg.error(Gui.class, "Error setting LookAndFeel: " + lookAndFeel.getName()); + Msg.error(Gui.class, "Error setting LookAndFeel: " + lookAndFeel.getName(), e); } - refresh(); } } - private static void refresh() { - GColor.refreshAll(); + public static void addTheme(GTheme newTheme) { + allThemes.remove(newTheme); + allThemes.add(newTheme); + } + + private static void updateUIs() { for (Window window : Window.getWindows()) { SwingUtilities.updateComponentTreeUI(window); } } -// private static GThemeValueMap convertJavaDefaults(GThemeValueMap input) { -// GThemeValueMap converted = new GThemeValueMap(); -// for (ColorValue colorValue : input.getColors()) { -// converted.addColor(fromUiResource(colorValue)); -// } -// for (FontValue fontValue : input.getFonts()) { -// converted.addFont(fromUiResource(fontValue)); -// } -// // java icons are not currently supported -// return converted; -// } - - private static FontValue fromUiResource(FontValue fontValue) { - Font font = fontValue.getRawValue(); - if (font instanceof UIResource) { - return new FontValue(fontValue.getId(), font.deriveFont(font.getStyle())); - } - return fontValue; - } - - private static ColorValue fromUiResource(ColorValue colorValue) { - Color color = colorValue.getRawValue(); - if (color instanceof UIResource) { - return new ColorValue(colorValue.getId(), new Color(color.getRGB(), true)); - } - return colorValue; - } - public static boolean isJavaDefinedColor(String id) { - return originalJavaDefaults.containsColor(id); + return javaDefaults.containsColor(id); } public static GThemeValueMap getAllValues() { return new GThemeValueMap(currentValues); } - public static GThemeValueMap getAllDefaultValues() { - GThemeValueMap currentDefaults = new GThemeValueMap(); - currentDefaults.load(originalJavaDefaults); - currentDefaults.load(ghidraCoreDefaults); - if (activeTheme.isDark()) { - currentDefaults.load(darkDefaults); - } - return currentDefaults; - } - public static Set getAllThemes() { if (allThemes == null) { allThemes = findThemes(); @@ -196,7 +182,7 @@ public class Gui { return new GIcon(id); } - public static void saveThemeToPreferneces(GTheme theme) { + public static void saveThemeToPreferences(GTheme theme) { Preferences.setProperty(THEME_PREFFERENCE_KEY, theme.getThemeLocater()); Preferences.store(); } @@ -205,7 +191,7 @@ public class Gui { return activeTheme; } - public static LookAndFeelType getLookAndFeelType() { + public static LafType getLookAndFeelType() { return activeTheme.getLookAndFeelType(); } @@ -269,16 +255,18 @@ public class Gui { return t; } - private static GThemeValueMap buildCurrentValues(GTheme theme) { + private static void buildCurrentValues() { GThemeValueMap map = new GThemeValueMap(); - map.load(originalJavaDefaults); - map.load(ghidraCoreDefaults); - if (theme.isDark()) { - map.load(darkDefaults); + map.load(javaDefaults); + map.load(ghidraLightDefaults); + if (activeTheme.isDark()) { + map.load(ghidraDarkDefaults); } - map.load(theme); - return map; + map.load(activeTheme); + currentValues = map; + GColor.refreshAll(); + repaintAll(); } private static Color getUIColor(String id) { @@ -306,7 +294,7 @@ public class Gui { List fileList = new ArrayList<>(); File dir = Application.getUserSettingsDirectory(); - FileFilter themeFileFilter = file -> file.getName().endsWith(".theme"); + FileFilter themeFileFilter = file -> file.getName().endsWith(GTheme.FILE_EXTENSION); fileList.addAll(Arrays.asList(dir.listFiles(themeFileFilter))); List list = new ArrayList<>(); @@ -359,29 +347,19 @@ public class Gui { return new DefaultTheme(); } - public static GThemeValueMap getCoreDefaults() { - GThemeValueMap map = new GThemeValueMap(ghidraCoreDefaults); - map.load(originalJavaDefaults); - return map; - } - - public static GThemeValueMap getDarkDefaults() { - GThemeValueMap map = new GThemeValueMap(ghidraCoreDefaults); - map.load(darkDefaults); - return map; - } - public static void setColor(String id, Color color) { setColor(new ColorValue(id, color)); } public static void setColor(ColorValue colorValue) { currentValues.addColor(colorValue); - System.out.println("Change color: " + colorValue); GColor.refreshAll(); + repaintAll(); + } + + private static void repaintAll() { for (Window window : Window.getWindows()) { window.repaint(); -// SynthLookAndFeel.updateStyles(window); } } @@ -394,13 +372,45 @@ public class Gui { return gColor; } - public static void setJavaDefaults(GThemeValueMap javaDefaults) { - originalJavaDefaults = javaDefaults; - currentValues = buildCurrentValues(activeTheme); + public static void setJavaDefaults(GThemeValueMap map) { + javaDefaults = map; + buildCurrentValues(); + } + + public static void fixupJavaDefaults() { + List colors = javaDefaults.getColors(); + JavaColorMapping mapping = new JavaColorMapping(); + for (ColorValue value : colors) { + ColorValue mapped = mapping.map(javaDefaults, value); + if (mapped != null) { + javaDefaults.addColor(mapped); + } + } } public static GThemeValueMap getJavaDefaults() { - return originalJavaDefaults; + GThemeValueMap map = new GThemeValueMap(); + map.load(javaDefaults); + return map; } + public static GThemeValueMap getGhidraDarkDefaults() { + GThemeValueMap map = new GThemeValueMap(ghidraLightDefaults); + map.load(ghidraDarkDefaults); + return map; + } + + public static GThemeValueMap getGhidraLightDefaults() { + GThemeValueMap map = new GThemeValueMap(ghidraLightDefaults); + return map; + } + + public static GThemeValueMap getDefaults() { + GThemeValueMap currentDefaults = new GThemeValueMap(javaDefaults); + currentDefaults.load(ghidraLightDefaults); + if (activeTheme.isDark()) { + currentDefaults.load(ghidraDarkDefaults); + } + return currentDefaults; + } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/LafType.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/LafType.java new file mode 100644 index 0000000000..e9729af1bd --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/LafType.java @@ -0,0 +1,123 @@ +/* ### + * 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.theme; + +import javax.swing.UIManager; +import javax.swing.UIManager.LookAndFeelInfo; + +import docking.theme.laf.*; +import ghidra.framework.OperatingSystem; +import ghidra.framework.Platform; +import ghidra.util.exception.AssertException; + +public enum LafType { + METAL("Metal", false), + NIMBUS("Nimbus", false), + GTK("GTK+", false), + MOTIF("CDE/Motif", false), + FLAT_LIGHT("Flat Light", false), + FLAT_DARK("Flat Dark", true), + FLAT_DARCULA("Flat Darcula", true), + WINDOWS("Windows", false), + WINDOWS_CLASSIC("Windows Classic", false), + MAC("Mac OS X", false), + SYSTEM("System", false); + + private String name; + private boolean isDark; + + private LafType(String name, boolean isDark) { + this.name = name; + this.isDark = isDark; + } + + public String getName() { + return name; + } + + public boolean isDark() { + return isDark; + } + + public static LafType fromName(String name) { + for (LafType type : values()) { + if (type.getName().equals(name)) { + return type; + } + } + return null; + } + + private static LookAndFeelInstaller getSystemLookAndFeelInstaller() { + OperatingSystem OS = Platform.CURRENT_PLATFORM.getOperatingSystem(); + if (OS == OperatingSystem.LINUX) { + return getInstaller(NIMBUS); + } + else if (OS == OperatingSystem.MAC_OS_X) { + return getInstaller(MAC); + } + else if (OS == OperatingSystem.WINDOWS) { + return getInstaller(WINDOWS); + } + return getInstaller(NIMBUS); + } + + public boolean isSupported() { + if (this == SYSTEM) { + return true; + } + LookAndFeelInfo[] installedLookAndFeels = UIManager.getInstalledLookAndFeels(); + for (LookAndFeelInfo info : installedLookAndFeels) { + if (name.equals(info.getName())) { + return true; + } + } + return false; + } + + public void install() throws Exception { + getInstaller(this).install(); + } + + private static LookAndFeelInstaller getInstaller(LafType lookAndFeel) { + switch (lookAndFeel) { + case FLAT_DARCULA: + return new FlatLookAndFeelInstaller(FLAT_DARCULA); + case FLAT_DARK: + return new FlatLookAndFeelInstaller(FLAT_DARK); + case FLAT_LIGHT: + return new FlatLookAndFeelInstaller(FLAT_LIGHT); + case GTK: + return new GTKLookAndFeelInstaller(); + case MAC: + return new LookAndFeelInstaller(MAC); + case METAL: + return new LookAndFeelInstaller(METAL); + case MOTIF: + return new MotifLookAndFeelInstaller(); // Motif has some specific ui fix ups + case NIMBUS: + return new NimbusLookAndFeelInstaller(); // Nimbus installs a special way + case SYSTEM: + return getSystemLookAndFeelInstaller(); + case WINDOWS: + return new LookAndFeelInstaller(WINDOWS); + case WINDOWS_CLASSIC: + return new LookAndFeelInstaller(WINDOWS_CLASSIC); + default: + throw new AssertException("No lookAndFeelInstaller defined for " + lookAndFeel); + } + } +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/LookAndFeelType.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/LookAndFeelType.java deleted file mode 100644 index 4df2aae601..0000000000 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/LookAndFeelType.java +++ /dev/null @@ -1,80 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package docking.theme; - -import com.formdev.flatlaf.*; - -import docking.theme.laf.*; -import ghidra.framework.OperatingSystem; -import ghidra.framework.Platform; - -public enum LookAndFeelType { - METAL("Metal", new MetalLookAndFeelInstaller()), - NIMBUS("Nimbus", new NimbusLookAndFeelInstaller()), - GTK("GTK+", new GTKLookAndFeelInstaller()), - MOTIF("CDE/Motif", new MotifLookAndFeelInstaller()), - FLAT_LIGHT("Flat Light", new FlatLookAndFeelInstaller(new FlatLightLaf())), - FLAT_DARK("Flat Dark", new FlatLookAndFeelInstaller(new FlatDarkLaf())), - FLAT_DARCULA("Flat Light", new FlatLookAndFeelInstaller(new FlatDarculaLaf())), - WINDOWS("Windows", new WindowsLookAndFeelInstaller()), - WINDOWS_CLASSIC("Windows Classic", new WindowsClassicLookAndFeelInstaller()), - MAC("Mac OS X", new MacLookAndFeelInstaller()), - SYSTEM("System", getSystemLookAndFeelInstaller()); - - private String name; - private LookAndFeelInstaller installer; - - private LookAndFeelType(String name, LookAndFeelInstaller installer) { - this.name = name; - this.installer = installer; - } - - public String getName() { - return name; - } - - public static LookAndFeelType fromName(String name) { - for (LookAndFeelType type : values()) { - if (type.getName().equals(name)) { - return type; - } - } - return null; - } - - private static LookAndFeelInstaller getSystemLookAndFeelInstaller() { - OperatingSystem OS = Platform.CURRENT_PLATFORM.getOperatingSystem(); - if (OS == OperatingSystem.LINUX) { - return NIMBUS.installer; - } - else if (OS == OperatingSystem.MAC_OS_X) { - return MAC.installer; - } - else if (OS == OperatingSystem.WINDOWS) { - return WINDOWS.installer; - } - return NIMBUS.installer; - } - - public boolean isSupported() { - return installer.isSupportedForCurrentPlatform(); - } - - public void install() throws Exception { - installer.install(); - } - -} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/ThemeReader.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/ThemeReader.java index cfafcb885a..4de3a0413e 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/ThemeReader.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/ThemeReader.java @@ -22,8 +22,7 @@ public class ThemeReader extends ThemePropertyFileReader { private Section themeSection; private String themeName; - private LookAndFeelType lookAndFeel; - private boolean isDark; + private LafType lookAndFeel; public ThemeReader(File file) throws IOException { super(file); @@ -37,12 +36,11 @@ public class ThemeReader extends ThemePropertyFileReader { throw new IOException("Missing theme name!"); } String lookAndFeelName = section.getValue(GTheme.THEME_LOOK_AND_FEEL_KEY); - lookAndFeel = LookAndFeelType.fromName(lookAndFeelName); + lookAndFeel = LafType.fromName(lookAndFeelName); if (lookAndFeel == null) { throw new IOException( "Invalid or missing lookAndFeel name: \"" + lookAndFeelName + "\""); } - isDark = Boolean.parseBoolean(section.getValue(GTheme.THEME_IS_DARK_KEY)); } void loadValues(GTheme theme) { @@ -50,7 +48,6 @@ public class ThemeReader extends ThemePropertyFileReader { // processValues expects only colors, fonts, and icons themeSection.remove(GTheme.THEME_NAME_KEY); themeSection.remove(GTheme.THEME_LOOK_AND_FEEL_KEY); - themeSection.remove(GTheme.THEME_IS_DARK_KEY); processValues(theme, themeSection); } @@ -59,11 +56,7 @@ public class ThemeReader extends ThemePropertyFileReader { return themeName; } - public boolean isDark() { - return isDark; - } - - public LookAndFeelType getLookAndFeelType() { + public LafType getLookAndFeelType() { return lookAndFeel; } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/CDEMotifTheme.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/CDEMotifTheme.java index 0cee78efe4..67039acf94 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/CDEMotifTheme.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/CDEMotifTheme.java @@ -16,12 +16,12 @@ package docking.theme.builtin; import docking.theme.DiscoverableGTheme; -import docking.theme.LookAndFeelType; +import docking.theme.LafType; public class CDEMotifTheme extends DiscoverableGTheme { public CDEMotifTheme() { - super("Motif", LookAndFeelType.MOTIF); + super("Motif", LafType.MOTIF); } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/FlatDarculaTheme.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/FlatDarculaTheme.java index b0f2e1c54b..b588903aad 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/FlatDarculaTheme.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/FlatDarculaTheme.java @@ -16,10 +16,10 @@ package docking.theme.builtin; import docking.theme.DiscoverableGTheme; -import docking.theme.LookAndFeelType; +import docking.theme.LafType; public class FlatDarculaTheme extends DiscoverableGTheme { public FlatDarculaTheme() { - super("Flat Darcula", LookAndFeelType.FLAT_DARCULA, true); + super("Flat Darcula", LafType.FLAT_DARCULA); } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/FlatDarkTheme.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/FlatDarkTheme.java index 3ebfd04738..e5f371f3bf 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/FlatDarkTheme.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/FlatDarkTheme.java @@ -16,10 +16,10 @@ package docking.theme.builtin; import docking.theme.DiscoverableGTheme; -import docking.theme.LookAndFeelType; +import docking.theme.LafType; public class FlatDarkTheme extends DiscoverableGTheme { public FlatDarkTheme() { - super("Flat Dark", LookAndFeelType.FLAT_DARK, true); + super("Flat Dark", LafType.FLAT_DARK); } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/FlatLightTheme.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/FlatLightTheme.java index 77bddc9393..d6fc1a6d61 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/FlatLightTheme.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/FlatLightTheme.java @@ -16,12 +16,12 @@ package docking.theme.builtin; import docking.theme.DiscoverableGTheme; -import docking.theme.LookAndFeelType; +import docking.theme.LafType; public class FlatLightTheme extends DiscoverableGTheme { public FlatLightTheme() { - super("Flat Light", LookAndFeelType.FLAT_LIGHT); + super("Flat Light", LafType.FLAT_LIGHT); } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/GTKTheme.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/GTKTheme.java index ebf2d3c800..9962473088 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/GTKTheme.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/GTKTheme.java @@ -16,12 +16,12 @@ package docking.theme.builtin; import docking.theme.DiscoverableGTheme; -import docking.theme.LookAndFeelType; +import docking.theme.LafType; public class GTKTheme extends DiscoverableGTheme { public GTKTheme() { - super("GDK+", LookAndFeelType.GTK); + super("GTK+", LafType.GTK); } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/JavaColorMapping.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/JavaColorMapping.java new file mode 100644 index 0000000000..88cee0607b --- /dev/null +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/JavaColorMapping.java @@ -0,0 +1,246 @@ +/* ### + * 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.theme.builtin; + +import java.awt.Color; +import java.util.HashMap; +import java.util.Map; + +import docking.theme.ColorValue; +import docking.theme.GThemeValueMap; + +/** + * Maps Java UIDefaults color ids to parent color ids + */ +public class JavaColorMapping { + private Map map = new HashMap<>(); + + public JavaColorMapping() { + // color relationships mined from BasicLookAndFeel + map.put("Button.background", "control"); + map.put("Button.foreground", "controlText"); + map.put("Button.shadow", "controlShadow"); + map.put("Button.darkShadow", "controlDkShadow"); + map.put("Button.light", "controlHighlight"); + map.put("Button.highlight", "controlLtHighlight"); + map.put("ToggleButton.background", "control"); + map.put("ToggleButton.foreground", "controlText"); + map.put("ToggleButton.shadow", "controlShadow"); + map.put("ToggleButton.darkShadow", "controlDkShadow"); + map.put("ToggleButton.light", "controlHighlight"); + map.put("ToggleButton.highlight", "controlLtHighlight"); + map.put("RadioButton.background", "control"); + map.put("RadioButton.foreground", "controlText"); + map.put("RadioButton.shadow", "controlShadow"); + map.put("RadioButton.darkShadow", "controlDkShadow"); + map.put("RadioButton.light", "controlHighlight"); + map.put("RadioButton.highlight", "controlLtHighlight"); + map.put("CheckBox.background", "control"); + map.put("CheckBox.foreground", "controlText"); + map.put("ColorChooser.background", "control"); + map.put("ColorChooser.foreground", "controlText"); + map.put("ColorChooser.swatchesDefaultRecentColor", "control"); + map.put("ComboBox.background", "window"); + map.put("ComboBox.foreground", "textText"); + map.put("ComboBox.buttonBackground", "control"); + map.put("ComboBox.buttonShadow", "controlShadow"); + map.put("ComboBox.buttonDarkShadow", "controlDkShadow"); + map.put("ComboBox.buttonHighlight", "controlLtHighlight"); + map.put("ComboBox.selectionBackground", "textHighlight"); + map.put("ComboBox.selectionForeground", "textHighlightText"); + map.put("ComboBox.disabledBackground", "control"); + map.put("ComboBox.disabledForeground", "textHInactiveText"); + map.put("InternalFrame.borderColor", "control"); + map.put("InternalFrame.borderShadow", "controlShadow"); + map.put("InternalFrame.borderDarkShadow", "controlDkShadow"); + map.put("InternalFrame.borderHighlight", "controlLtHighlight"); + map.put("InternalFrame.borderLight", "controlHighlight"); + map.put("InternalFrame.activeTitleBackground", "activeCaption"); + map.put("InternalFrame.activeTitleForeground", "activeCaptionText"); + map.put("InternalFrame.inactiveTitleBackground", "inactiveCaption"); + map.put("InternalFrame.inactiveTitleForeground", "inactiveCaptionText"); + map.put("Label.background", "control"); + map.put("Label.foreground", "controlText"); + map.put("Label.disabledShadow", "controlShadow"); + map.put("List.background", "window"); + map.put("List.foreground", "textText"); + map.put("List.selectionBackground", "textHighlight"); + map.put("List.selectionForeground", "textHighlightText"); + map.put("List.dropLineColor", "controlShadow"); + map.put("MenuBar.background", "menu"); + map.put("MenuBar.foreground", "menuText"); + map.put("MenuBar.shadow", "controlShadow"); + map.put("MenuBar.highlight", "controlLtHighlight"); + map.put("MenuItem.background", "menu"); + map.put("MenuItem.foreground", "menuText"); + map.put("MenuItem.selectionForeground", "textHighlightText"); + map.put("MenuItem.selectionBackground", "textHighlight"); + map.put("MenuItem.acceleratorForeground", "menuText"); + map.put("MenuItem.acceleratorSelectionForeground", "textHighlightText"); + map.put("RadioButtonMenuItem.background", "menu"); + map.put("RadioButtonMenuItem.foreground", "menuText"); + map.put("RadioButtonMenuItem.selectionForeground", "textHighlightText"); + map.put("RadioButtonMenuItem.selectionBackground", "textHighlight"); + map.put("RadioButtonMenuItem.acceleratorForeground", "menuText"); + map.put("RadioButtonMenuItem.acceleratorSelectionForeground", "textHighlightText"); + map.put("CheckBoxMenuItem.background", "menu"); + map.put("CheckBoxMenuItem.foreground", "menuText"); + map.put("CheckBoxMenuItem.selectionForeground", "textHighlightText"); + map.put("CheckBoxMenuItem.selectionBackground", "textHighlight"); + map.put("CheckBoxMenuItem.acceleratorForeground", "menuText"); + map.put("CheckBoxMenuItem.acceleratorSelectionForeground", "textHighlightText"); + map.put("Menu.background", "menu"); + map.put("Menu.foreground", "menuText"); + map.put("Menu.selectionForeground", "textHighlightText"); + map.put("Menu.selectionBackground", "textHighlight"); + map.put("Menu.acceleratorForeground", "menuText"); + map.put("Menu.acceleratorSelectionForeground", "textHighlightText"); + map.put("PopupMenu.background", "menu"); + map.put("PopupMenu.foreground", "menuText"); + map.put("OptionPane.background", "control"); + map.put("OptionPane.foreground", "controlText"); + map.put("OptionPane.messageForeground", "controlText"); + map.put("Panel.background", "control"); + map.put("Panel.foreground", "textText"); + map.put("ProgressBar.foreground", "textHighlight"); + map.put("ProgressBar.background", "control"); + map.put("ProgressBar.selectionForeground", "control"); + map.put("ProgressBar.selectionBackground", "textHighlight"); + map.put("Separator.background", "controlLtHighlight"); + map.put("Separator.foreground", "controlShadow"); + map.put("ScrollBar.foreground", "control"); + map.put("ScrollBar.track", "scrollbar"); + map.put("ScrollBar.trackHighlight", "controlDkShadow"); + map.put("ScrollBar.thumb", "control"); + map.put("ScrollBar.thumbHighlight", "controlLtHighlight"); + map.put("ScrollBar.thumbDarkShadow", "controlDkShadow"); + map.put("ScrollBar.thumbShadow", "controlShadow"); + map.put("ScrollPane.background", "control"); + map.put("ScrollPane.foreground", "controlText"); + map.put("Viewport.background", "control"); + map.put("Viewport.foreground", "textText"); + map.put("Slider.foreground", "control"); + map.put("Slider.background", "control"); + map.put("Slider.highlight", "controlLtHighlight"); + map.put("Slider.shadow", "controlShadow"); + map.put("Slider.focus", "controlDkShadow"); + map.put("Spinner.background", "control"); + map.put("Spinner.foreground", "control"); + map.put("SplitPane.background", "control"); + map.put("SplitPane.highlight", "controlLtHighlight"); + map.put("SplitPane.shadow", "controlShadow"); + map.put("SplitPane.darkShadow", "controlDkShadow"); + map.put("TabbedPane.background", "control"); + map.put("TabbedPane.foreground", "controlText"); + map.put("TabbedPane.highlight", "controlLtHighlight"); + map.put("TabbedPane.light", "controlHighlight"); + map.put("TabbedPane.shadow", "controlShadow"); + map.put("TabbedPane.darkShadow", "controlDkShadow"); + map.put("TabbedPane.focus", "controlText"); + map.put("Table.foreground", "controlText"); + map.put("Table.background", "window"); + map.put("Table.selectionForeground", "textHighlightText"); + map.put("Table.selectionBackground", "textHighlight"); + map.put("Table.dropLineColor", "controlShadow"); + map.put("Table.focusCellBackground", "window"); + map.put("Table.focusCellForeground", "controlText"); + map.put("TableHeader.foreground", "controlText"); + map.put("TableHeader.background", "control"); + map.put("TableHeader.focusCellBackground", "text"); + map.put("TextField.background", "window"); + map.put("TextField.foreground", "textText"); + map.put("TextField.shadow", "controlShadow"); + map.put("TextField.darkShadow", "controlDkShadow"); + map.put("TextField.light", "controlHighlight"); + map.put("TextField.highlight", "controlLtHighlight"); + map.put("TextField.inactiveForeground", "textHInactiveText"); + map.put("TextField.inactiveBackground", "control"); + map.put("TextField.selectionBackground", "textHighlight"); + map.put("TextField.selectionForeground", "textHighlightText"); + map.put("TextField.caretForeground", "textText"); + map.put("FormattedTextField.background", "window"); + map.put("FormattedTextField.foreground", "textText"); + map.put("FormattedTextField.inactiveForeground", "textHInactiveText"); + map.put("FormattedTextField.inactiveBackground", "control"); + map.put("FormattedTextField.selectionBackground", "textHighlight"); + map.put("FormattedTextField.selectionForeground", "textHighlightText"); + map.put("FormattedTextField.caretForeground", "textText"); + map.put("PasswordField.background", "window"); + map.put("PasswordField.foreground", "textText"); + map.put("PasswordField.inactiveForeground", "textHInactiveText"); + map.put("PasswordField.inactiveBackground", "control"); + map.put("PasswordField.selectionBackground", "textHighlight"); + map.put("PasswordField.selectionForeground", "textHighlightText"); + map.put("PasswordField.caretForeground", "textText"); + map.put("TextArea.background", "window"); + map.put("TextArea.foreground", "textText"); + map.put("TextArea.inactiveForeground", "textHInactiveText"); + map.put("TextArea.selectionBackground", "textHighlight"); + map.put("TextArea.selectionForeground", "textHighlightText"); + map.put("TextArea.caretForeground", "textText"); + map.put("TextPane.foreground", "textText"); + map.put("TextPane.selectionBackground", "textHighlight"); + map.put("TextPane.selectionForeground", "textHighlightText"); + map.put("TextPane.caretForeground", "textText"); + map.put("TextPane.inactiveForeground", "textHInactiveText"); + map.put("EditorPane.foreground", "textText"); + map.put("EditorPane.selectionBackground", "textHighlight"); + map.put("EditorPane.selectionForeground", "textHighlightText"); + map.put("EditorPane.caretForeground", "textText"); + map.put("EditorPane.inactiveForeground", "textHInactiveText"); + map.put("TitledBorder.titleColor", "controlText"); + map.put("ToolBar.background", "control"); + map.put("ToolBar.foreground", "controlText"); + map.put("ToolBar.shadow", "controlShadow"); + map.put("ToolBar.darkShadow", "controlDkShadow"); + map.put("ToolBar.light", "controlHighlight"); + map.put("ToolBar.highlight", "controlLtHighlight"); + map.put("ToolBar.dockingBackground", "control"); + map.put("ToolBar.floatingBackground", "control"); + map.put("ToolTip.background", "info"); + map.put("ToolTip.foreground", "infoText"); + map.put("Tree.background", "window"); + map.put("Tree.foreground", "textText"); + map.put("Tree.textForeground", "textText"); + map.put("Tree.textBackground", "text"); + map.put("Tree.selectionForeground", "textHighlightText"); + map.put("Tree.selectionBackground", "textHighlight"); + map.put("Tree.dropLineColor", "controlShadow"); + + } + + public ColorValue map(GThemeValueMap values, ColorValue value) { + String id = value.getId(); + String refId = map.get(id); + if (refId == null) { + return null; + } + ColorValue refValue = values.getColor(refId); + if (refValue == null) { + return null; + } + Color originalColor = value.get(values); + Color refColor = refValue.get(values); + if (originalColor == null || refColor == null) { + return null; + } + if (originalColor.getRGB() == refColor.getRGB()) { + return new ColorValue(id, refId); + } + return null; + } + +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/MacTheme.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/MacTheme.java index 19462cfe40..a3bc521527 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/MacTheme.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/MacTheme.java @@ -16,11 +16,11 @@ package docking.theme.builtin; import docking.theme.DiscoverableGTheme; -import docking.theme.LookAndFeelType; +import docking.theme.LafType; public class MacTheme extends DiscoverableGTheme { public MacTheme() { - super("Mac OS X", LookAndFeelType.MAC); + super("Mac OS X", LafType.MAC); } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/MetalTheme.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/MetalTheme.java index 9beff7ef2b..6df5600f91 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/MetalTheme.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/MetalTheme.java @@ -16,12 +16,12 @@ package docking.theme.builtin; import docking.theme.DiscoverableGTheme; -import docking.theme.LookAndFeelType; +import docking.theme.LafType; public class MetalTheme extends DiscoverableGTheme { public MetalTheme() { - super("Metal", LookAndFeelType.METAL); + super("Metal", LafType.METAL); } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/NimbusTheme.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/NimbusTheme.java index 42c2c30e8f..b2bb5cd889 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/NimbusTheme.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/NimbusTheme.java @@ -16,12 +16,12 @@ package docking.theme.builtin; import docking.theme.DiscoverableGTheme; -import docking.theme.LookAndFeelType; +import docking.theme.LafType; public class NimbusTheme extends DiscoverableGTheme { public NimbusTheme() { - super("Nimbus", LookAndFeelType.NIMBUS); + super("Nimbus", LafType.NIMBUS); } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/WindowsClassicTheme.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/WindowsClassicTheme.java index a243582b1d..e1a0760239 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/WindowsClassicTheme.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/WindowsClassicTheme.java @@ -16,11 +16,11 @@ package docking.theme.builtin; import docking.theme.DiscoverableGTheme; -import docking.theme.LookAndFeelType; +import docking.theme.LafType; public class WindowsClassicTheme extends DiscoverableGTheme { public WindowsClassicTheme() { - super("Windows Classic", LookAndFeelType.WINDOWS_CLASSIC); + super("Windows Classic", LafType.WINDOWS_CLASSIC); } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/WindowsTheme.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/WindowsTheme.java index cce9011803..fff8c632b3 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/WindowsTheme.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/builtin/WindowsTheme.java @@ -16,11 +16,11 @@ package docking.theme.builtin; import docking.theme.DiscoverableGTheme; -import docking.theme.LookAndFeelType; +import docking.theme.LafType; public class WindowsTheme extends DiscoverableGTheme { public WindowsTheme() { - super("Windows", LookAndFeelType.WINDOWS); + super("Windows", LafType.WINDOWS); } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/GThemeDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/GThemeDialog.java deleted file mode 100644 index 92de7c3196..0000000000 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/GThemeDialog.java +++ /dev/null @@ -1,200 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package docking.theme.gui; - -import java.awt.*; -import java.awt.event.*; -import java.util.Collections; -import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; - -import javax.swing.*; - -import docking.DialogComponentProvider; -import docking.theme.*; -import docking.widgets.combobox.GhidraComboBox; -import docking.widgets.table.GFilterTable; -import docking.widgets.table.GTable; -import ghidra.util.Swing; -import resources.Icons; - -public class GThemeDialog extends DialogComponentProvider { - - private ThemeColorTableModel colorTableModel; - private GThemeColorEditorDialog dialog; - - public GThemeDialog() { - super("Theme Dialog", false); - addWorkPanel(createMainPanel()); - addOKButton(); - addCancelButton(); - setOkButtonText("Save"); - setPreferredSize(1100, 500); - setRememberSize(false); - - } - - @Override - protected void okCallback() { - for (Window window : Window.getWindows()) { - SwingUtilities.updateComponentTreeUI(window); - } - -// GhidraFileChooser chooser = new GhidraFileChooser(getComponent()); -// chooser.setTitle("Choose Theme File"); -// chooser.setApproveButtonText("Select Output File"); -// chooser.setApproveButtonToolTipText("Select File"); -// chooser.setFileSelectionMode(GhidraFileChooserMode.FILES_ONLY); -// chooser.setSelectedFileFilter(GhidraFileFilter.ALL); -// File file = chooser.getSelectedFile(); -// try { -// Gui.getActiveTheme().saveToFile(file, Gui.getAllDefaultValues()); -// } -// catch (IOException e) { -// // TODO Auto-generated catch block -// e.printStackTrace(); -// } - } - - private JComponent createMainPanel() { - JPanel panel = new JPanel(); - - panel.setLayout(new BorderLayout()); - panel.add(buildControlPanel(), BorderLayout.NORTH); - panel.add(buildTabedTables()); - return panel; - } - - private Component buildControlPanel() { - JPanel panel = new JPanel(new BorderLayout()); - panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); -// panel.add(buildThemeChoiceButtons(), BorderLayout.WEST); - panel.add(buildThemeCombo(), BorderLayout.WEST); - panel.add(buildReloadDefaultsButton(), BorderLayout.EAST); - - return panel; - } - - private Component buildReloadDefaultsButton() { - JButton button = new JButton(Icons.REFRESH_ICON); - button.addActionListener(this::reloadThemeDefaults); - button.setToolTipText("Reload Theme Defaults"); - return button; - } - - private Component buildThemeCombo() { - JPanel panel = new JPanel(); - Set supportedThemes = Gui.getSupportedThemes(); - List themeNames = - supportedThemes.stream().map(t -> t.getName()).collect(Collectors.toList()); - Collections.sort(themeNames); - - GhidraComboBox combo = new GhidraComboBox<>(themeNames); - combo.setSelectedItem(Gui.getActiveTheme().getName()); - combo.addItemListener(this::themeComboChanged); - - panel.add(new JLabel("Theme: "), BorderLayout.WEST); - panel.add(combo); - panel.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 10)); - return panel; - } - - private Component buildThemeChoiceButtons() { - JPanel panel = new JPanel(new FlowLayout()); - panel.add(createThemeButton("Flat")); - panel.add(createThemeButton("Dark Flat")); - panel.add(createThemeButton("Metal")); - panel.add(createThemeButton("Nimbus")); - panel.add(createThemeButton("GDK+")); - panel.add(createThemeButton("CDE/Motif")); - return panel; - } - - private JButton createThemeButton(String name) { - JButton button = new JButton(name); - button.addActionListener(e -> Gui.setTheme(Gui.getTheme(name))); - return button; - } - - private Component buildTabedTables() { - JTabbedPane tabbedPane = new JTabbedPane(); - tabbedPane.add("Colors", buildColorTable()); - return tabbedPane; - } - - private JComponent buildColorTable() { - colorTableModel = new ThemeColorTableModel(); - - GFilterTable filterTable = new GFilterTable<>(colorTableModel); - GTable table = filterTable.getTable(); - table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); - - table.addKeyListener(new KeyAdapter() { - @Override - public void keyPressed(KeyEvent e) { - if (e.getKeyCode() == KeyEvent.VK_ENTER) { - ColorValue colorValue = filterTable.getSelectedRowObject(); - if (colorValue != null) { - editColor(colorValue); - } - e.consume(); - } - } - }); - - table.addMouseListener(new MouseAdapter() { - @Override - public void mouseClicked(MouseEvent e) { - if (e.getClickCount() == 2) { - ColorValue value = filterTable.getItemAt(e.getPoint()); - editColor(value); - } - } - }); - - return filterTable; - } - - private void themeComboChanged(ItemEvent e) { - if (e.getStateChange() == ItemEvent.SELECTED) { - String themeName = (String) e.getItem(); - Swing.runLater(() -> Gui.setTheme(Gui.getTheme(themeName))); - Swing.runLater(() -> colorTableModel.reload()); - } - } - - private void reloadThemeDefaults(ActionEvent e) { - Gui.reloadThemeDefaults(); - colorTableModel.reload(); - } - - protected void editColor(ColorValue value) { - if (dialog == null) { - dialog = new GThemeColorEditorDialog(this); - } - dialog.editColor(value); - } - - void colorChangeAccepted() { - colorTableModel.reload(); - } - - void colorEditorClosed() { - dialog = null; - } - -} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/GThemeColorEditorDialog.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeColorEditorDialog.java similarity index 74% rename from Ghidra/Framework/Docking/src/main/java/docking/theme/gui/GThemeColorEditorDialog.java rename to Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeColorEditorDialog.java index 1dd8a7e226..8c5333f36f 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/GThemeColorEditorDialog.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeColorEditorDialog.java @@ -26,18 +26,17 @@ import docking.DockingWindowManager; import docking.options.editor.GhidraColorChooser; import docking.theme.ColorValue; import docking.theme.Gui; -import ghidra.util.Swing; -public class GThemeColorEditorDialog extends DialogComponentProvider { +public class ThemeColorEditorDialog extends DialogComponentProvider { - private ColorValue originalColorValue; + private ColorValue startingColorValue; private ColorValue currentColorValue; - private GThemeDialog themeDialog; + private ThemeDialog themeDialog; private GhidraColorChooser colorChooser; private ChangeListener colorChangeListener = e -> colorChanged(); - public GThemeColorEditorDialog(GThemeDialog themeDialog) { + public ThemeColorEditorDialog(ThemeDialog themeDialog) { super("Theme Color Editor", false); this.themeDialog = themeDialog; addWorkPanel(buildColorPanel()); @@ -46,14 +45,11 @@ public class GThemeColorEditorDialog extends DialogComponentProvider { } public void editColor(ColorValue colorValue) { - if (currentColorValue != null && !currentColorValue.equals(originalColorValue)) { - themeDialog.colorChangeAccepted(); - } - this.originalColorValue = colorValue; + this.startingColorValue = colorValue; this.currentColorValue = colorValue; setTitle("Edit Color For: " + colorValue.getId()); - Color color = Gui.getRawColor(originalColorValue.getId()); + Color color = Gui.getRawColor(startingColorValue.getId()); colorChooser.getSelectionModel().removeChangeListener(colorChangeListener); colorChooser.setColor(color); colorChooser.getSelectionModel().addChangeListener(colorChangeListener); @@ -74,11 +70,8 @@ public class GThemeColorEditorDialog extends DialogComponentProvider { @Override protected void okCallback() { - if (!currentColorValue.equals(originalColorValue)) { - themeDialog.colorChangeAccepted(); - } currentColorValue = null; - originalColorValue = null; + startingColorValue = null; close(); themeDialog.colorEditorClosed(); } @@ -87,19 +80,21 @@ public class GThemeColorEditorDialog extends DialogComponentProvider { protected void cancelCallback() { restoreOriginalColor(); currentColorValue = null; - originalColorValue = null; + startingColorValue = null; close(); themeDialog.colorEditorClosed(); } private void restoreOriginalColor() { - Gui.setColor(originalColorValue); + themeDialog.colorChanged(currentColorValue, startingColorValue); + currentColorValue = startingColorValue; } private void colorChanged() { Color newColor = colorChooser.getColor(); - currentColorValue = new ColorValue(originalColorValue.getId(), newColor); - Swing.runLater(() -> Gui.setColor(currentColorValue)); + ColorValue newColorValue = new ColorValue(startingColorValue.getId(), newColor); + themeDialog.colorChanged(currentColorValue, newColorValue); + currentColorValue = newColorValue; } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeColorTableModel.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeColorTableModel.java index e298f03712..8cd09a19c7 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeColorTableModel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/gui/ThemeColorTableModel.java @@ -28,31 +28,43 @@ import docking.widgets.table.*; import ghidra.docking.settings.Settings; import ghidra.framework.plugintool.ServiceProvider; import ghidra.framework.plugintool.ServiceProviderStub; +import ghidra.util.ColorUtils; import ghidra.util.WebColors; import ghidra.util.table.column.AbstractGColumnRenderer; import ghidra.util.table.column.GColumnRenderer; public class ThemeColorTableModel extends GDynamicColumnTableModel { private List colors; - private GThemeValueMap values; - private GThemeValueMap coreDefaults; - private GThemeValueMap darkDefaults; + private GThemeValueMap currentValues; + private GThemeValueMap themeValues; + private GThemeValueMap defaultValues; + private GThemeValueMap lightDefaultValues; + private GThemeValueMap darkDefaultValues; public ThemeColorTableModel() { super(new ServiceProviderStub()); - loadValues(); + load(); } - public void reload() { - loadValues(); + public void reloadCurrent() { + currentValues = Gui.getAllValues(); + colors = currentValues.getColors(); fireTableDataChanged(); } - private void loadValues() { - values = Gui.getAllValues(); - coreDefaults = Gui.getCoreDefaults(); - darkDefaults = Gui.getDarkDefaults(); - colors = values.getColors(); + public void reloadAll() { + load(); + fireTableDataChanged(); + } + + public void load() { + currentValues = Gui.getAllValues(); + colors = currentValues.getColors(); + themeValues = new GThemeValueMap(currentValues); + defaultValues = Gui.getDefaults(); + lightDefaultValues = Gui.getGhidraLightDefaults(); + darkDefaultValues = Gui.getGhidraDarkDefaults(); + } @Override @@ -69,10 +81,11 @@ public class ThemeColorTableModel extends GDynamicColumnTableModel createTableColumnDescriptor() { TableColumnDescriptor descriptor = new TableColumnDescriptor<>(); descriptor.addVisibleColumn(new IdColumn()); - descriptor.addVisibleColumn(new ValueColumn("Current Color", () -> values)); - descriptor.addVisibleColumn(new ValueColumn("Core Defaults", () -> coreDefaults)); - descriptor.addVisibleColumn(new ValueColumn("Dark Defaults", () -> darkDefaults)); - descriptor.addVisibleColumn(new IsLafPropertyColumn()); + descriptor.addVisibleColumn(new ValueColumn("Current Color", () -> currentValues)); + descriptor.addVisibleColumn(new ValueColumn("Theme Color", () -> themeValues)); + descriptor.addVisibleColumn(new ValueColumn("Default Color", () -> defaultValues)); + descriptor.addHiddenColumn(new ValueColumn("Light Defaults", () -> lightDefaultValues)); + descriptor.addHiddenColumn(new ValueColumn("Dark Defaults", () -> darkDefaultValues)); return descriptor; } @@ -100,7 +113,7 @@ public class ThemeColorTableModel extends GDynamicColumnTableModel { + class ValueColumn extends AbstractDynamicTableColumn { private ThemeColorRenderer renderer; private String name; private Supplier valueSupplier; @@ -108,7 +121,7 @@ public class ThemeColorTableModel extends GDynamicColumnTableModel supplier) { this.name = name; this.valueSupplier = supplier; - renderer = new ThemeColorRenderer(supplier); + renderer = new ThemeColorRenderer(); } @Override @@ -117,28 +130,35 @@ public class ThemeColorTableModel extends GDynamicColumnTableModel getColumnRenderer() { + public GColumnRenderer getColumnRenderer() { return renderer; } - public Comparator getComparator() { + public Comparator getComparator() { return (v1, v2) -> { - GThemeValueMap valueMap = valueSupplier.get(); - ColorValue v1Color = valueMap.getColor(v1.getId()); - ColorValue v2Color = valueMap.getColor(v2.getId()); - if (v1Color == null && v2Color == null) { + if (v1 == null && v2 == null) { return 0; } - if (v1Color == null) { + if (v1 == null) { return 1; } - return v1Color.compareValue(v2Color); + if (v2 == null) { + return -1; + } + return ColorUtils.COMPARATOR.compare(v1.color, v2.color); }; } @@ -146,74 +166,47 @@ public class ThemeColorTableModel extends GDynamicColumnTableModel { + private class ThemeColorRenderer extends AbstractGColumnRenderer { - @Override - public String getColumnName() { - return "Is Laf"; - } - - @Override - public Boolean getValue(ColorValue themeColor, Settings settings, Object data, - ServiceProvider provider) throws IllegalArgumentException { - return Gui.isJavaDefinedColor(themeColor.getId()); - } - - @Override - public int getColumnPreferredWidth() { - return 20; - } - } - - private class ThemeColorRenderer extends AbstractGColumnRenderer { - - private Supplier mapSupplier; - - public ThemeColorRenderer(Supplier mapSupplier) { - this.mapSupplier = mapSupplier; + public ThemeColorRenderer() { setFont(new Font("Monospaced", Font.PLAIN, 12)); } @Override public Component getTableCellRendererComponent(GTableCellRenderingData data) { - GThemeValueMap valueMap = mapSupplier.get(); JLabel label = (JLabel) super.getTableCellRendererComponent(data); - String id = ((ColorValue) data.getValue()).getId(); + ResolvedColor resolved = (ResolvedColor) data.getValue(); - ColorValue colorValue = valueMap.getColor(id); - Color color; - String text; - if (colorValue != null) { - color = colorValue.get(valueMap); - if (colorValue.getReferenceId() != null) { - text = colorValue.getReferenceId(); - } - else { - text = WebColors.toString(color, false); - String name = WebColors.toWebColorName(color); - if (name != null) { - text += " [" + name + "]"; - } - } - - } - else { - color = GThemeDefaults.Colors.BACKGROUND; - text = ""; - } + String text = getValueText(resolved); + Color color = resolved == null ? GThemeDefaults.Colors.BACKGROUND : resolved.color; label.setText(text); label.setIcon(new SwatchIcon(color, label.getForeground())); -// label.setBackground(color); -// label.setForeground(ColorUtils.contrastForegroundColor(color)); label.setOpaque(true); return label; } + private String getValueText(ResolvedColor resolvedColor) { + if (resolvedColor == null) { + return ""; + } + if (resolvedColor.refId != null) { + return resolvedColor.refId; + } + Color color = resolvedColor.color; + String text = WebColors.toString(color, false); + String name = WebColors.toWebColorName(color); + if (name != null) { + text += " [" + name + "]"; + } + return text; + } + @Override - public String getFilterString(ColorValue t, Settings settings) { - return t.getId(); + public String getFilterString(ResolvedColor colorValue, Settings settings) { + return getValueText(colorValue); } } @@ -244,6 +237,17 @@ public class ThemeColorTableModel extends GDynamicColumnTableModel combo; + private ItemListener comboListener = this::themeComboChanged; + + public ThemeDialog() { + super("Theme Dialog", false); + addWorkPanel(createMainPanel()); + + addDismissButton(); + addButton(createSaveButton()); + addButton(createRestoreButton()); + + setPreferredSize(1100, 500); + setRememberSize(false); + updateButtons(); + } + + @Override + protected void dismissCallback() { + if (hasChanges()) { + int result = OptionDialog.showYesNoCancelDialog(null, "Close Theme Dialog", + "You have changed the theme.\n Do you want save your changes?"); + if (result == OptionDialog.CANCEL_OPTION) { + return; + } + if (result == OptionDialog.YES_OPTION) { + if (!save()) { + return; + } + } + else { + Gui.reloadGhidraDefaults(); + } + } + INSTANCE = null; + close(); + } + + protected void saveCallback() { + save(); + updateCombo(); + } + + private void restoreCallback() { + if (hasChanges()) { + int result = OptionDialog.showYesNoDialog(null, "Restore Theme Values", + "Are you sure you want to discard all your changes?"); + if (result == OptionDialog.NO_OPTION) { + return; + } + } + Gui.restoreThemeValues(); + reset(); + } + + private void reloadDefaultsCallback(ActionEvent e) { + if (hasChanges()) { + int result = OptionDialog.showYesNoDialog(null, "Reload Ghidra Default Values", + "This will discard all your theme changes. Continue?"); + if (result == OptionDialog.NO_OPTION) { + return; + } + } + Gui.reloadGhidraDefaults(); + reset(); + } + + private void reset() { + changedValuesMap.clear(); + colorTableModel.reloadAll(); + updateButtons(); + } + + /** + * Saves all current theme changes + * @return true if the operation was not cancelled. + */ + private boolean save() { + GTheme activeTheme = Gui.getActiveTheme(); + if (activeTheme instanceof FileGTheme) { + FileGTheme fileTheme = (FileGTheme) activeTheme; + if (fileTheme.canSave()) { + int result = OptionDialog.showYesNoCancelDialog(null, "Overwrite Existing Theme", + "Do you want to overwrite the existing theme file?"); + if (result == OptionDialog.CANCEL_OPTION) { + return false; + } + if (result == OptionDialog.YES_OPTION) { + return saveCurrentValuesToTheme(fileTheme, false); + } + } + } + // save to new Theme file + + InputDialog inputDialog = new InputDialog("Create Theme", "New Theme Name"); + DockingWindowManager.showDialog(inputDialog); + String themeName = inputDialog.getValue(); + if (themeName == null) { + return false; + } + File file = getSaveFile(themeName); + LafType laf = activeTheme.getLookAndFeelType(); + return saveCurrentValuesToTheme(new FileGTheme(file, themeName, laf), false); + } + + private boolean saveCurrentValuesToTheme(FileGTheme newTheme, boolean includeDefaults) { + newTheme.clear(); + GThemeValueMap allValues = Gui.getAllValues(); + if (includeDefaults) { + newTheme.load(allValues); + } + else { + Gui.getAllValues(); + newTheme.load(allValues.removeSameValues(Gui.getDefaults())); + } + try { + newTheme.save(); + Gui.addTheme(newTheme); + Gui.setTheme(newTheme); + } + catch (IOException e) { + Msg.showError(this, null, "I/O Error", + "Error writing theme file: " + newTheme.getFile().getAbsolutePath(), e); + return false; + } + + return true; + + } + + private File getSaveFile(String themeName) { + File dir = Application.getUserSettingsDirectory(); + String cleanedName = themeName.replaceAll(" ", "_") + GTheme.FILE_EXTENSION; + return new File(dir, cleanedName); + } + +// private void export() { +// GhidraFileChooser chooser = new GhidraFileChooser(getComponent()); +// chooser.setTitle("Choose Theme File"); +// chooser.setApproveButtonText("Select Output File"); +// chooser.setApproveButtonToolTipText("Select File"); +// chooser.setFileSelectionMode(GhidraFileChooserMode.FILES_ONLY); +// chooser.setSelectedFileFilter(GhidraFileFilter.ALL); +// File file = chooser.getSelectedFile(); +// try { +// Gui.getActiveTheme().saveToFile(file, Gui.getDefaults()); +// return true; +// } +// catch (IOException e) { +// // TODO Auto-generated catch block +// e.printStackTrace(); +// } +// return false; +// } + + private void themeComboChanged(ItemEvent e) { + if (e.getStateChange() == ItemEvent.SELECTED) { + String themeName = (String) e.getItem(); + if (hasChanges()) { + Msg.debug(this, "has changes"); + } + Swing.runLater(() -> { + Gui.setTheme(Gui.getTheme(themeName)); + changedValuesMap.clear(); + colorTableModel.reloadAll(); + }); + } + } + + private boolean hasChanges() { + return !changedValuesMap.isEmpty(); + } + + protected void editColor(ColorValue value) { + if (dialog == null) { + dialog = new ThemeColorEditorDialog(this); + } + dialog.editColor(value); + } + + void colorChanged(ColorValue oldValue, ColorValue newValue) { + updateChanagedValueMap(oldValue, newValue); + // run later - don't rock the boat in the middle of a listener callback + Swing.runLater(() -> { + Gui.setColor(newValue); + colorTableModel.reloadCurrent(); + }); + } + + private void updateChanagedValueMap(ColorValue oldValue, ColorValue newValue) { + ColorValue originalValue = changedValuesMap.getColor(oldValue.getId()); + if (originalValue == null) { + changedValuesMap.addColor(oldValue); + } + else if (originalValue.equals(newValue)) { + // if restoring the original color, remove it from the map of changes + changedValuesMap.removeColor(oldValue.getId()); + } + updateButtons(); + } + + private void updateButtons() { + boolean hasChanges = hasChanges(); + saveButton.setEnabled(hasChanges); + restoreButton.setEnabled(hasChanges); + } + + void colorEditorClosed() { + dialog = null; + } + + private JComponent createMainPanel() { + JPanel panel = new JPanel(); + + panel.setLayout(new BorderLayout()); + panel.add(buildControlPanel(), BorderLayout.NORTH); + panel.add(buildTabedTables()); + return panel; + } + + private Component buildControlPanel() { + JPanel panel = new JPanel(new BorderLayout()); + panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10)); + panel.add(buildThemeCombo(), BorderLayout.WEST); + panel.add(buildReloadDefaultsButton(), BorderLayout.EAST); + panel.setName("gthemePanel"); + return panel; + } + + private Component buildReloadDefaultsButton() { + JButton button = new JButton(Icons.REFRESH_ICON); + button.addActionListener(this::reloadDefaultsCallback); + button.setToolTipText( + "Reload Ghidra Defaults (Only needed if you change a theme.properties file)"); + return button; + } + + private void updateCombo() { + Set supportedThemes = Gui.getSupportedThemes(); + List themeNames = + supportedThemes.stream().map(t -> t.getName()).collect(Collectors.toList()); + Collections.sort(themeNames); + combo.removeItemListener(comboListener); + combo.setModel(new DefaultComboBoxModel(new Vector(themeNames))); + combo.setSelectedItem(Gui.getActiveTheme().getName()); + combo.addItemListener(comboListener); + } + + private Component buildThemeCombo() { + JPanel panel = new JPanel(); + Set supportedThemes = Gui.getSupportedThemes(); + List themeNames = + supportedThemes.stream().map(t -> t.getName()).collect(Collectors.toList()); + Collections.sort(themeNames); + + combo = new GhidraComboBox<>(themeNames); + combo.setSelectedItem(Gui.getActiveTheme().getName()); + combo.addItemListener(comboListener); + + panel.add(new JLabel("Theme: "), BorderLayout.WEST); + panel.add(combo); + panel.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 10)); + return panel; + } + + private Component buildTabedTables() { + JTabbedPane tabbedPane = new JTabbedPane(); + tabbedPane.add("Colors", buildColorTable()); + return tabbedPane; + } + + private JComponent buildColorTable() { + colorTableModel = new ThemeColorTableModel(); + + GFilterTable filterTable = new GFilterTable<>(colorTableModel); + GTable table = filterTable.getTable(); + table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + + table.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + if (e.getKeyCode() == KeyEvent.VK_ENTER) { + ColorValue colorValue = filterTable.getSelectedRowObject(); + if (colorValue != null) { + editColor(colorValue); + } + e.consume(); + } + } + }); + + table.addMouseListener(new MouseAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + if (e.getClickCount() == 2) { + ColorValue value = filterTable.getItemAt(e.getPoint()); + Object cellValue = filterTable.getCellValue(e.getPoint()); +// editColor(value); + + int col = filterTable.getColumn(e.getPoint()); + TableColumn column = table.getColumnModel().getColumn(col); + Object identifier = column.getIdentifier(); + if ("Current Color".equals(identifier) || "Id".equals(identifier)) { + editColor(value); + } + } + } + }); + + return filterTable; + } + + private JButton createRestoreButton() { + restoreButton = new JButton("Restore"); + restoreButton.setMnemonic('R'); + restoreButton.setName("Restore"); + restoreButton.addActionListener(e -> restoreCallback()); + restoreButton.setToolTipText("Restores all values to current theme"); + return restoreButton; + } + + private JButton createSaveButton() { + saveButton = new JButton("Save"); + saveButton.setMnemonic('S'); + saveButton.setName("Save"); + saveButton.addActionListener(e -> saveCallback()); + saveButton.setToolTipText("Saves changed values to a new Theme"); + return saveButton; + } + + public static void editTheme() { + if (INSTANCE != null) { + INSTANCE.toFront(); + return; + } + INSTANCE = new ThemeDialog(); + DockingWindowManager.showDialog(INSTANCE); + + } + +} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/FlatLookAndFeelInstaller.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/FlatLookAndFeelInstaller.java index 4c75a06ab1..049b94c6e2 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/FlatLookAndFeelInstaller.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/FlatLookAndFeelInstaller.java @@ -16,25 +16,22 @@ package docking.theme.laf; import javax.swing.UIManager; -import javax.swing.UnsupportedLookAndFeelException; -import com.formdev.flatlaf.FlatLaf; +import docking.theme.LafType; public class FlatLookAndFeelInstaller extends LookAndFeelInstaller { - private FlatLaf lookAndFeel; - public FlatLookAndFeelInstaller(FlatLaf lookAndFeel) { - this.lookAndFeel = lookAndFeel; + public FlatLookAndFeelInstaller(LafType lookAndFeelType) { + super(lookAndFeelType); } @Override - protected void installLookAndFeel() throws UnsupportedLookAndFeelException { - UIManager.setLookAndFeel(lookAndFeel); - } + protected void fixupLookAndFeelIssues() { + super.fixupLookAndFeelIssues(); - @Override - public boolean isSupportedForCurrentPlatform() { - return true; + // We have historically managed button focusability ourselves. Allow this by default so + // features continue to work as expected, such as right-clicking on ToolButtons. + UIManager.put("ToolBar.focusableButtons", Boolean.TRUE); } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/GTKLookAndFeelInstaller.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/GTKLookAndFeelInstaller.java index 2b8bce53af..6d663d6a05 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/GTKLookAndFeelInstaller.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/GTKLookAndFeelInstaller.java @@ -15,68 +15,29 @@ */ package docking.theme.laf; -import java.awt.Color; -import java.util.List; - import javax.swing.*; -import javax.swing.plaf.synth.SynthLookAndFeel; -import docking.theme.*; -import ghidra.docking.util.LookAndFeelUtils; +import docking.theme.LafType; public class GTKLookAndFeelInstaller extends LookAndFeelInstaller { + public GTKLookAndFeelInstaller() { + super(LafType.GTK); + } + + @Override + protected void installLookAndFeel() throws ClassNotFoundException, InstantiationException, + IllegalAccessException, UnsupportedLookAndFeelException { + + super.installLookAndFeel(); + LookAndFeel lookAndFeel = UIManager.getLookAndFeel(); + WrappingLookAndFeel wrappingLookAndFeel = new WrappingLookAndFeel(lookAndFeel); + UIManager.setLookAndFeel(wrappingLookAndFeel); + } + @Override protected void installJavaDefaults() { - // do nothing - already handled by wrapped GTK lookAndFeel - } - - @Override - protected void installLookAndFeel() throws UnsupportedLookAndFeelException { - String name = LookAndFeelType.GTK.getName(); - try { - UIManager.setLookAndFeel(findLookAndFeelClassName(name)); - LookAndFeel gtk = UIManager.getLookAndFeel(); - UIManager.setLookAndFeel(new WrappingLookAndFeel(gtk)); - } - catch (Exception e) { - throw new UnsupportedLookAndFeelException(name + " not supported on this platform"); - } - } - - @Override - public boolean isSupportedForCurrentPlatform() { - return isSupported(LookAndFeelType.GTK.getName()); - } - - /** - * Extends the NimbusLookAndFeel to intercept the {@link #getDefaults()}. To get Nimbus - * to use our indirect values, we have to get in early. - */ - static class ExtendedGTKLookAndFeel extends SynthLookAndFeel { - - @Override - public UIDefaults getDefaults() { - GThemeValueMap javaDefaults = new GThemeValueMap(); - - UIDefaults defaults = super.getDefaults(); - List colorIds = - LookAndFeelUtils.getLookAndFeelIdsForType(defaults, Color.class); - for (String id : colorIds) { - Color color = defaults.getColor(id); - ColorValue value = new ColorValue(id, color); - javaDefaults.addColor(value); - } - Gui.setJavaDefaults(javaDefaults); - for (String id : colorIds) { - defaults.put(id, Gui.getGColorUiResource(id)); - } -// javaDefaults.addColor(new ColorValue("Label.textForground", "Label.foreground")); - defaults.put("Label.textForeground", Gui.getGColorUiResource("Label.foreground")); - GColor.refreshAll(); - return defaults; - } - + // handled by WrappingLookAndFeel } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/LookAndFeelInstaller.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/LookAndFeelInstaller.java index f8225c01ad..8aa67784e0 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/LookAndFeelInstaller.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/LookAndFeelInstaller.java @@ -22,15 +22,37 @@ import java.util.Map.Entry; import javax.swing.*; import javax.swing.UIManager.LookAndFeelInfo; -import javax.swing.plaf.UIResource; import docking.theme.*; import ghidra.docking.util.LookAndFeelUtils; import ghidra.util.*; -public abstract class LookAndFeelInstaller { +/** + * Installs a specific {@link LookAndFeel} into the {@link UIManager}. The idea is that there + * is a specific installer for each supported {@link LookAndFeel} to handle unique needs for + * that LookAndFeel. Subclasses can also override {@link #fixupLookAndFeelIssues()} to make + * UI tweaks to specific LookAndFeels. + */ +public class LookAndFeelInstaller { - public void install() throws Exception { + private LafType lookAndFeel; + + public LookAndFeelInstaller(LafType lookAndFeel) { + this.lookAndFeel = lookAndFeel; + } + + /** + * Installs the {@link LookAndFeel} associated with this installer + * @throws ClassNotFoundException if the LookAndFeel + * class could not be found + * @throws InstantiationException if a new instance of the class + * couldn't be created + * @throws IllegalAccessException if the class or initializer isn't accessible + * @throws UnsupportedLookAndFeelException if + * lnf.isSupportedLookAndFeel() is false + */ + public void install() throws ClassNotFoundException, InstantiationException, + IllegalAccessException, UnsupportedLookAndFeelException { cleanUiDefaults(); installLookAndFeel(); installJavaDefaults(); @@ -38,21 +60,34 @@ public abstract class LookAndFeelInstaller { installGlobalProperties(); } - private void installGlobalProperties() { - installGlobalLookAndFeelAttributes(); - installGlobalFontSizeOverride(); - installCustomLookAndFeelActions(); - installPopupMenuSettingsOverride(); + /** + * Subclass provide this method to install the specific loo + * @throws ClassNotFoundException if the LookAndFeel + * class could not be found + * @throws InstantiationException if a new instance of the class + * couldn't be created + * @throws IllegalAccessException if the class or initializer isn't accessible + * @throws UnsupportedLookAndFeelException if + * lnf.isSupportedLookAndFeel() is false + */ + protected void installLookAndFeel() throws ClassNotFoundException, InstantiationException, + IllegalAccessException, UnsupportedLookAndFeelException { + String name = lookAndFeel.getName(); + UIManager.setLookAndFeel(findLookAndFeelClassName(name)); + } - public abstract boolean isSupportedForCurrentPlatform(); - - protected abstract void installLookAndFeel() throws Exception; - + /** + * Subclass can override this method to do specific LookAndFeel fix ups + */ protected void fixupLookAndFeelIssues() { // no generic fix-ups at this time. } + /** + * Installs GColors into the UIDefaults. Subclasses my override this if they need to install + * GColors in a different way. + */ protected void installJavaDefaults() { GThemeValueMap javaDefaults = extractJavaDefaults(); Gui.setJavaDefaults(javaDefaults); @@ -82,36 +117,14 @@ public abstract class LookAndFeelInstaller { List ids = LookAndFeelUtils.getLookAndFeelIdsForType(UIManager.getDefaults(), Color.class); for (String id : ids) { - values.addColor(new ColorValue(id, getNonUiColor(id))); + // only use standard java colors here to avoid weird issues (such as GColor not + // resolving or ColorUIResource not being honored. Later we will go back + // and fix up the java defaults to use standard java color indirection + values.addColor(new ColorValue(id, getNormalizedColor(UIManager.getColor(id)))); } return values; } - private static Color getNonUiColor(String id) { - // Not sure, but for now, make sure colors are not UIResource - Color color = UIManager.getColor(id); - if (color instanceof UIResource) { - return new Color(color.getRGB(), true); - } - return color; - } - - private void cleanUiDefaults() { - GThemeValueMap javaDefaults = Gui.getJavaDefaults(); - if (javaDefaults == null) { - return; - } - UIDefaults defaults = UIManager.getDefaults(); - for (ColorValue colorValue : javaDefaults.getColors()) { - String id = colorValue.getId(); - defaults.put(id, null); - } -// for (FontValue fontValue : javaDefaults.getFonts()) { -// String id = fontValue.getId(); -// defaults.put(id, null); -// } - } - protected String findLookAndFeelClassName(String lookAndFeelName) { LookAndFeelInfo[] installedLookAndFeels = UIManager.getInstalledLookAndFeels(); for (LookAndFeelInfo info : installedLookAndFeels) { @@ -233,4 +246,34 @@ public abstract class LookAndFeelInstaller { inputMap.put(keyStroke, action); } } + + private void installGlobalProperties() { + installGlobalLookAndFeelAttributes(); + installGlobalFontSizeOverride(); + installCustomLookAndFeelActions(); + installPopupMenuSettingsOverride(); + } + + private static Color getNormalizedColor(Color color) { + if (color.getClass() == Color.class) { + return color; + } + return new Color(color.getRGB(), true); + } + + private void cleanUiDefaults() { + GThemeValueMap javaDefaults = Gui.getJavaDefaults(); + if (javaDefaults == null) { + return; + } + UIDefaults defaults = UIManager.getDefaults(); + for (ColorValue colorValue : javaDefaults.getColors()) { + String id = colorValue.getId(); + defaults.put(id, null); + } +// for (FontValue fontValue : javaDefaults.getFonts()) { +// String id = fontValue.getId(); +// defaults.put(id, null); +// } + } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/MacLookAndFeelInstaller.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/MacLookAndFeelInstaller.java deleted file mode 100644 index 3ea38de18d..0000000000 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/MacLookAndFeelInstaller.java +++ /dev/null @@ -1,35 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package docking.theme.laf; - -import javax.swing.UIManager; - -import docking.theme.LookAndFeelType; - -public class MacLookAndFeelInstaller extends LookAndFeelInstaller { - - @Override - public boolean isSupportedForCurrentPlatform() { - return isSupported(LookAndFeelType.MAC.getName()); - } - - @Override - protected void installLookAndFeel() throws Exception { - String name = LookAndFeelType.MAC.getName(); - UIManager.setLookAndFeel(findLookAndFeelClassName(name)); - } - -} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/MetalLookAndFeelInstaller.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/MetalLookAndFeelInstaller.java deleted file mode 100644 index 006713de04..0000000000 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/MetalLookAndFeelInstaller.java +++ /dev/null @@ -1,34 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package docking.theme.laf; - -import javax.swing.UIManager; -import javax.swing.UnsupportedLookAndFeelException; -import javax.swing.plaf.metal.MetalLookAndFeel; - -public class MetalLookAndFeelInstaller extends LookAndFeelInstaller { - - @Override - protected void installLookAndFeel() throws UnsupportedLookAndFeelException { - UIManager.setLookAndFeel(new MetalLookAndFeel()); - } - - @Override - public boolean isSupportedForCurrentPlatform() { - return true; - } - -} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/MotifLookAndFeelInstaller.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/MotifLookAndFeelInstaller.java index 6f1cd2462c..ad63db6781 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/MotifLookAndFeelInstaller.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/MotifLookAndFeelInstaller.java @@ -15,21 +15,12 @@ */ package docking.theme.laf; -import javax.swing.UIManager; - -import docking.theme.LookAndFeelType; +import docking.theme.LafType; public class MotifLookAndFeelInstaller extends LookAndFeelInstaller { - @Override - public boolean isSupportedForCurrentPlatform() { - return true; - } - - @Override - protected void installLookAndFeel() throws Exception { - String name = LookAndFeelType.MOTIF.getName(); - UIManager.setLookAndFeel(findLookAndFeelClassName(name)); + public MotifLookAndFeelInstaller() { + super(LafType.MOTIF); } @Override diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/NimbusLookAndFeelInstaller.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/NimbusLookAndFeelInstaller.java index e1f18dd74b..d252ae652c 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/NimbusLookAndFeelInstaller.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/NimbusLookAndFeelInstaller.java @@ -27,14 +27,13 @@ import ghidra.docking.util.LookAndFeelUtils; public class NimbusLookAndFeelInstaller extends LookAndFeelInstaller { - @Override - protected void installLookAndFeel() throws UnsupportedLookAndFeelException { - UIManager.setLookAndFeel(new GNimbusLookAndFeel()); + public NimbusLookAndFeelInstaller() { + super(LafType.NIMBUS); } @Override - public boolean isSupportedForCurrentPlatform() { - return true; + protected void installLookAndFeel() throws UnsupportedLookAndFeelException { + UIManager.setLookAndFeel(new GNimbusLookAndFeel()); } @Override diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/WindowsClassicLookAndFeelInstaller.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/WindowsClassicLookAndFeelInstaller.java deleted file mode 100644 index 04e383e3dd..0000000000 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/WindowsClassicLookAndFeelInstaller.java +++ /dev/null @@ -1,35 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package docking.theme.laf; - -import javax.swing.UIManager; - -import docking.theme.LookAndFeelType; - -public class WindowsClassicLookAndFeelInstaller extends LookAndFeelInstaller { - - @Override - public boolean isSupportedForCurrentPlatform() { - return isSupported(LookAndFeelType.WINDOWS_CLASSIC.getName()); - } - - @Override - protected void installLookAndFeel() throws Exception { - String name = LookAndFeelType.WINDOWS_CLASSIC.getName(); - UIManager.setLookAndFeel(findLookAndFeelClassName(name)); - } - -} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/WindowsLookAndFeelInstaller.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/WindowsLookAndFeelInstaller.java deleted file mode 100644 index 87c0ad8439..0000000000 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/WindowsLookAndFeelInstaller.java +++ /dev/null @@ -1,35 +0,0 @@ -/* ### - * IP: GHIDRA - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package docking.theme.laf; - -import javax.swing.UIManager; - -import docking.theme.LookAndFeelType; - -public class WindowsLookAndFeelInstaller extends LookAndFeelInstaller { - - @Override - public boolean isSupportedForCurrentPlatform() { - return isSupported(LookAndFeelType.WINDOWS.getName()); - } - - @Override - protected void installLookAndFeel() throws Exception { - String name = LookAndFeelType.WINDOWS.getName(); - UIManager.setLookAndFeel(findLookAndFeelClassName(name)); - } - -} diff --git a/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/WrappingLookAndFeel.java b/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/WrappingLookAndFeel.java index 885ad7c4c9..46d250cfbd 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/WrappingLookAndFeel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/theme/laf/WrappingLookAndFeel.java @@ -45,6 +45,7 @@ public class WrappingLookAndFeel extends LookAndFeel { Gui.setJavaDefaults(javaDefaults); for (String id : colorIds) { defaults.put(id, Gui.getGColorUiResource(id)); +// defaults.put(id, new GColor(id)); } defaults.put("Label.textForeground", Gui.getGColorUiResource("Label.foreground")); GColor.refreshAll(); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GFilterTable.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GFilterTable.java index 49c52d80fd..6791b12215 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GFilterTable.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/GFilterTable.java @@ -201,4 +201,18 @@ public class GFilterTable extends JPanel { public void setFiterText(String text) { filterPanel.setFilterText(text); } + + public int getRow(Point point) { + return table.rowAtPoint(point); + } + + public int getColumn(Point point) { + return table.columnAtPoint(point); + } + + public Object getCellValue(Point point) { + int row = getRow(point); + int col = getColumn(point); + return table.getValueAt(row, col); + } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/TableUtils.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/TableUtils.java index 8e21218f3e..4b17fc608c 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/TableUtils.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/table/TableUtils.java @@ -41,8 +41,7 @@ public class TableUtils { * @return the string value; null if no value can be fabricated */ public static String getTableCellStringValue(RowObjectTableModel model, - ROW_OBJECT rowObject, - int column) { + ROW_OBJECT rowObject, int column) { // note: this call can be slow when columns dynamically calculate values from the database Object value = model.getColumnValueForRow(rowObject, column); @@ -87,12 +86,13 @@ public class TableUtils { private static String getRenderedColumnValue(RowObjectTableModel model, Object columnValue, int columnIndex) { - if (!(model instanceof DynamicColumnTableModel)) { + TableModel unwrappedModel = RowObjectTableModel.unwrap(model); + if (!(unwrappedModel instanceof DynamicColumnTableModel)) { return null; } DynamicColumnTableModel columnBasedModel = - (DynamicColumnTableModel) model; + (DynamicColumnTableModel) unwrappedModel; GColumnRenderer renderer = getColumnRenderer(columnBasedModel, columnIndex); if (renderer == null) { return null; 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 be987aa5ab..8ff01579f8 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 @@ -23,7 +23,7 @@ import javax.swing.plaf.ComponentUI; import org.apache.commons.collections4.IteratorUtils; -import docking.theme.LookAndFeelType; +import docking.theme.LafType; /** * A utility class to manage LookAndFeel (LaF) settings. @@ -80,7 +80,7 @@ public class LookAndFeelUtils { */ public static boolean isUsingNimbusUI() { LookAndFeel lookAndFeel = UIManager.getLookAndFeel(); - return LookAndFeelType.NIMBUS.equals(lookAndFeel.getName()); + return LafType.NIMBUS.equals(lookAndFeel.getName()); } } diff --git a/Ghidra/Framework/Docking/src/main/java/ghidra/util/ReservedKeyBindings.java b/Ghidra/Framework/Docking/src/main/java/ghidra/util/ReservedKeyBindings.java index 025c116c48..64dee72fa9 100644 --- a/Ghidra/Framework/Docking/src/main/java/ghidra/util/ReservedKeyBindings.java +++ b/Ghidra/Framework/Docking/src/main/java/ghidra/util/ReservedKeyBindings.java @@ -48,6 +48,10 @@ public class ReservedKeyBindings { public static final KeyStroke UPDATE_KEY_BINDINGS_KEY = KeyStroke.getKeyStroke(KeyEvent.VK_F4, 0); + public static final KeyStroke COMPONENT_THEME_INFO_KEY = KeyStroke.getKeyStroke( + KeyEvent.VK_F9, DockingUtils.CONTROL_KEY_MODIFIER_MASK | + InputEvent.ALT_DOWN_MASK | InputEvent.SHIFT_DOWN_MASK); + public static boolean isReservedKeystroke(KeyStroke keyStroke) { int code = keyStroke.getKeyCode(); if (code == KeyEvent.VK_SHIFT || code == KeyEvent.VK_ALT || code == KeyEvent.VK_CONTROL || @@ -55,6 +59,7 @@ public class ReservedKeyBindings { HELP_KEY1.equals(keyStroke) || HELP_KEY2.equals(keyStroke) || HELP_INFO_KEY.equals(keyStroke) || UPDATE_KEY_BINDINGS_KEY.equals(keyStroke) || FOCUS_INFO_KEY.equals(keyStroke) || FOCUS_CYCLE_INFO_KEY.equals(keyStroke) || + COMPONENT_THEME_INFO_KEY.equals(keyStroke) || CONTEXT_MENU_KEY1.equals(keyStroke) || CONTEXT_MENU_KEY2.equals(keyStroke)) { return true; } diff --git a/Ghidra/Framework/Docking/src/test/java/docking/theme/GThemeTest.java b/Ghidra/Framework/Docking/src/test/java/docking/theme/GThemeTest.java index 8170e8caa7..ddeeb3a45d 100644 --- a/Ghidra/Framework/Docking/src/test/java/docking/theme/GThemeTest.java +++ b/Ghidra/Framework/Docking/src/test/java/docking/theme/GThemeTest.java @@ -92,10 +92,11 @@ public class GThemeTest extends AbstractGenericTest { File file = createTempFile("themeTest.theme"); - theme = theme.saveToFile(file); // saveToFile returns new theme instance + theme.saveToFile(file, false); // saveToFile returns new theme instance + theme = new FileGTheme(file); assertEquals("abc", theme.getName()); - assertEquals(LookAndFeelType.SYSTEM, theme.getLookAndFeelType()); + assertEquals(LafType.SYSTEM, theme.getLookAndFeelType()); assertEquals(Color.RED, theme.getColor("color.a.1").get(theme)); assertEquals(Color.BLUE, theme.getColor("color.a.2").get(theme)); diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/ColorUtils.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/ColorUtils.java index cbdf0848aa..46b3d9cdef 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/ColorUtils.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/ColorUtils.java @@ -16,6 +16,7 @@ package ghidra.util; import java.awt.Color; +import java.util.Comparator; public class ColorUtils { @@ -202,6 +203,31 @@ public class ColorUtils { return new Color(c.getRed(), c.getGreen(), c.getBlue(), alpha); } + /** + * A color {@link Comparator} for ordering colors. + */ + public static Comparator COMPARATOR = new Comparator() { + + @Override + public int compare(Color c1, Color c2) { + int alpha1 = c1.getAlpha(); + int alpha2 = c2.getAlpha(); + + if (alpha1 == alpha2) { + return getHsbCompareValue(c1) - getHsbCompareValue(c2); + } + return alpha1 - alpha2; + } + + private int getHsbCompareValue(Color v) { + // compute a value the compares colors first by hue, then saturation, then brightness + // reduce noise by converting float values from 0-1 to integers 0 - 7 + float[] hsb = Color.RGBtoHSB(v.getRed(), v.getGreen(), v.getBlue(), null); + return 100 * (int) (10 * hsb[0]) + 10 * (int) (10 * hsb[1]) + (int) (10 * hsb[2]); + } + + }; + /** * Blender of colors */ diff --git a/Ghidra/Framework/Graph/data/graph.theme.properties b/Ghidra/Framework/Graph/data/graph.theme.properties index 3c2fd5bf80..6c5d57f8a9 100644 --- a/Ghidra/Framework/Graph/data/graph.theme.properties +++ b/Ghidra/Framework/Graph/data/graph.theme.properties @@ -6,16 +6,51 @@ color.bg.visualgraph.satellite = lightgray color.bg.highlight.visualgraph = rgba(255,255,0,155) // somewhat transparent yellow color.bg.visualgraph.message = rgb(138, 185, 241) // jordy blue +color.bg.visualgraph.satellite.vertex = color.palette.material.secondary.variant color.fg.visualgraph.message = color.palette.black +// TODO maybe make this darker for light mode; it is bright against a bright background +color.bg.visualgraph.dockingvertex = color.palette.material.secondary +color.fg.visualgraph.dockingvertex = black +color.visualgraph.dockingvertex.cursor = red + +color.visualgraph.view.primary.edge.draw = color.palette.green // draw and emphasized +color.visualgraph.view.primary.edge.focused = color.palette.green // when an edge is in a focused path +color.visualgraph.view.primary.edge.selected = color.palette.lime // when an edge is selected +color.visualgraph.view.primary.edge.hovered = color.palette.lime // dashed lines; when an edge is in the hovered path +color.visualgraph.view.satellite.edge.draw = color.palette.green +color.visualgraph.view.satellite.edge.focused = color.palette.green +color.visualgraph.view.satellite.edge.selected = color.palette.lime +color.visualgraph.view.satellite.edge.hovered = color.palette.lime + + + + # graph display color.graphdisplay.vertex = green color.graphdisplay.edge = green color.graphdisplay.vertex.selected = blue color.graphdisplay.edge.selected = blue + [Dark Defaults] color.bg.highlight.visualgraph = rgba(120,120,120,155) // light gray with dark bg = dark gray color.bg.visualgraph.message = rgb(65, 146, 242) // dark blue close to jordy blue color.fg.visualgraph.message = color.palette.lightgray + +color.bg.visualgraph.satellite.vertex = color.palette.material.secondary.variant + +color.bg.visualgraph.dockingvertex = color.palette.material.secondary.variant +color.fg.visualgraph.dockingvertex = black // unchanged +color.visualgraph.dockingvertex.cursor = pink // arbitrary + +color.visualgraph.view.primary.edge.draw = #75CCB9 // color.palette.green // draw and emphasized +color.visualgraph.view.primary.edge.focused = #75CCB9 // color.palette.green // when an edge is in a focused path +color.visualgraph.view.primary.edge.selected = #BBFFCC // when an edge is selected +color.visualgraph.view.primary.edge.hovered = #BBFFCC // dashed lines; when an edge is in the hovered path +color.visualgraph.view.satellite.edge.draw = #75CCB9 +color.visualgraph.view.satellite.edge.focused = #75CCB9 +color.visualgraph.view.satellite.edge.selected = #BBFFCC +color.visualgraph.view.satellite.edge.hovered = #BBFFCC + diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/GraphComponent.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/GraphComponent.java index 9ce4e6287f..727532b56d 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/GraphComponent.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/GraphComponent.java @@ -45,6 +45,7 @@ import edu.uci.ics.jung.visualization.picking.PickedState; import edu.uci.ics.jung.visualization.picking.ShapePickSupport; import edu.uci.ics.jung.visualization.renderers.BasicEdgeRenderer; import edu.uci.ics.jung.visualization.renderers.Renderer; +import edu.uci.ics.jung.visualization.renderers.Renderer.Vertex; import edu.uci.ics.jung.visualization.util.Caching; import ghidra.graph.VisualGraph; import ghidra.graph.event.VisualGraphChangeListener; @@ -253,17 +254,22 @@ public class GraphComponent, G e RenderContext renderContext = viewer.getRenderContext(); - Color normal = Palette.GREEN; - Color selected = Palette.LIME; + GColor drawColor = new GColor("color.visualgraph.view.primary.edge.draw"); + GColor focusedColor = new GColor("color.visualgraph.view.primary.edge.focused"); + GColor selectedColor = new GColor("color.visualgraph.view.primary.edge.selected"); + GColor hoveredColor = new GColor("color.visualgraph.view.primary.edge.hovered"); if (edgeRenderer instanceof VisualEdgeRenderer) { - VisualEdgeRenderer visualRenderer = + VisualEdgeRenderer visualEdgeRenderer = (VisualEdgeRenderer) renderer.getEdgeRenderer(); - visualRenderer.setDrawColorTransformer(e -> normal); - visualRenderer.setSelectedColorTransformer(e -> selected); + visualEdgeRenderer.setDrawColorTransformer(e -> drawColor); + visualEdgeRenderer.setFocusedColorTransformer(e -> focusedColor); + visualEdgeRenderer.setSelectedColorTransformer(e -> selectedColor); + visualEdgeRenderer.setHoveredColorTransformer(e -> hoveredColor); + } else { Function edgeColorTransformer = - e -> e.isSelected() ? selected : normal; + e -> e.isSelected() ? selectedColor : drawColor; renderContext.setEdgeDrawPaintTransformer(edgeColorTransformer); renderContext.setArrowDrawPaintTransformer(edgeColorTransformer); renderContext.setArrowFillPaintTransformer(edgeColorTransformer); @@ -331,15 +337,26 @@ public class GraphComponent, G e RenderContext renderContext = viewer.getRenderContext(); Renderer renderer = viewer.getRenderer(); - renderer.setVertexRenderer(viewer.getPreferredVertexRenderer()); + Vertex vertexRenderer = viewer.getPreferredVertexRenderer(); + + renderContext + .setVertexFillPaintTransformer( + v -> new GColor("color.bg.visualgraph.satellite.vertex")); + + renderer.setVertexRenderer(vertexRenderer); VisualGraphEdgeSatelliteRenderer visualEdgeRenderer = new VisualGraphEdgeSatelliteRenderer<>( (VisualEdgeRenderer) layout.getEdgeRenderer()); renderer.setEdgeRenderer(visualEdgeRenderer); - Color normal = Palette.GREEN; - Color selected = Palette.LIME; - visualEdgeRenderer.setDrawColorTransformer(e -> e.isSelected() ? selected : normal); + visualEdgeRenderer.setDrawColorTransformer( + e -> new GColor("color.visualgraph.view.satellite.edge.draw")); + visualEdgeRenderer.setFocusedColorTransformer( + e -> new GColor("color.visualgraph.view.satellite.edge.focused")); + visualEdgeRenderer.setSelectedColorTransformer( + e -> new GColor("color.visualgraph.view.satellite.edge.selected")); + visualEdgeRenderer.setHoveredColorTransformer( + e -> new GColor("color.visualgraph.view.satellite.edge.hovered")); Function edgeTransformer = layout.getEdgeShapeTransformer(); renderContext.setEdgeShapeTransformer(edgeTransformer); diff --git a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/edge/VisualEdgeRenderer.java b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/edge/VisualEdgeRenderer.java index bb73596f0d..586278d937 100644 --- a/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/edge/VisualEdgeRenderer.java +++ b/Ghidra/Framework/Graph/src/main/java/ghidra/graph/viewer/edge/VisualEdgeRenderer.java @@ -26,7 +26,6 @@ import com.google.common.base.Function; import com.google.common.base.Predicate; import docking.theme.GThemeDefaults.Colors.Palette; -import docking.theme.Gui; import edu.uci.ics.jung.algorithms.layout.Layout; import edu.uci.ics.jung.graph.Graph; import edu.uci.ics.jung.graph.util.Context; @@ -79,11 +78,13 @@ import ghidra.graph.viewer.vertex.VisualGraphVertexShapeTransformer; * offsets to handle vertex clipping. * *

When painting edges this renderer will paint colors based on the following states: default, - * hovered, focused and selected. A focused edge is one that is part of the path between focused - * vertices, whereas a selected edge is one that has been selected by the user (see - * {@link VisualEdge} for details). Each of these states may have a different color that can be - * changed by calling the various setter methods on this renderer. When painting, these colors - * are used along with various different strokes to paint in an overlay fashion. + * emphasized, hovered, focused and selected. A focused edge is one that is part of the path + * between focused vertices(such as when the vertex is hovered), whereas a selected edge is one + * that has been selected by the user (see {@link VisualEdge} for details). An edge is + * 'emphasized' when the user mouses over the edge (which is when the edge is hovered, not when the + * vertex is hovered. Each of these states may have a different color that can be changed by + * calling the various setter methods on this renderer. When painting, these colors are used along + * with various different strokes to paint in an overlay fashion. * * @param the vertex type * @param the edge type @@ -101,6 +102,7 @@ public abstract class VisualEdgeRenderer drawColorTransformer = e -> Palette.BLACK; private Function focusedColorTransformer = e -> Palette.GRAY; private Function selectedColorTransformer = e -> Palette.GRAY; + private Function hoveredColorTransformer = e -> Palette.LIGHT_GRAY; private VisualEdgeArrowRenderingSupport arrowRenderingSupport = new VisualEdgeArrowRenderingSupport<>(); @@ -118,7 +120,8 @@ public abstract class VisualEdgeRenderer transformer) { @@ -126,7 +129,7 @@ public abstract class VisualEdgeRenderer transformer) { + this.hoveredColorTransformer = Objects.requireNonNull(transformer); + } + + /** + * Returns the current color to use when the edge is in the hovered path. + * @param g the graph + * @param e the edge + * @return the color + */ + public Color getHoveredColor(Graph g, E e) { + return hoveredColorTransformer.apply(e); + } + // template method protected boolean isInHoveredVertexPath(E e) { return e.isInHoveredVertexPath(); @@ -219,10 +240,10 @@ public abstract class VisualEdgeRenderer