GP-3153 Added view for displaying theme colors in tree form to group similar colors. This is a intended to be used by Ghidra developers to consoldiate similar colors.

This commit is contained in:
ghidragon 2023-03-02 13:52:20 -05:00
parent 2e2bc48c4f
commit 582d646aa0
3 changed files with 525 additions and 0 deletions

View file

@ -0,0 +1,76 @@
/* ###
* 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.Color;
import java.util.Comparator;
import java.util.function.Function;
/**
* Class for sorting colors by rgb values. Each enum values changes the order of comparison for the
* red, green, and blue color values.
*/
public enum ColorSorter implements Comparator<Color> {
RGB("Red, Green, Blue", c -> c.getRed(), c -> c.getGreen(), c -> c.getBlue()),
RBG("Red, Blue, Green", c -> c.getRed(), c -> c.getBlue(), c -> c.getGreen()),
GRB("Green, Red, Blue", c -> c.getGreen(), c -> c.getRed(), c -> c.getBlue()),
GBR("Green, Blue, Red", c -> c.getGreen(), c -> c.getBlue(), c -> c.getRed()),
BRG("Blue, Red, Green", c -> c.getBlue(), c -> c.getRed(), c -> c.getGreen()),
BGR("Blue, Green, Red", c -> c.getBlue(), c -> c.getGreen(), c -> c.getRed());
private String name;
private ColorFunction colorFunction1;
private ColorFunction colorFunction2;
private ColorFunction colorFunction3;
ColorSorter(String name, ColorFunction f1, ColorFunction f2, ColorFunction f3) {
this.name = name;
colorFunction1 = f1;
colorFunction2 = f2;
colorFunction3 = f3;
}
public String getName() {
return name;
}
public String toString() {
return name;
}
@Override
public int compare(Color o1, Color o2) {
int v1 = colorFunction1.apply(o1);
int v2 = colorFunction1.apply(o2);
int result = v1 - v2;
if (result == 0) {
v1 = colorFunction2.apply(o1);
v2 = colorFunction2.apply(o2);
result = v1 - v2;
if (result == 0) {
v1 = colorFunction3.apply(o1);
v2 = colorFunction3.apply(o2);
result = v1 - v2;
}
}
return result;
}
interface ColorFunction extends Function<Color, Integer> {
//
}
}

View file

@ -0,0 +1,442 @@
/* ###
* 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 static docking.theme.gui.ColorSorter.*;
import java.awt.*;
import java.awt.event.ItemEvent;
import java.awt.event.MouseEvent;
import java.util.*;
import java.util.List;
import javax.swing.*;
import docking.ActionContext;
import docking.action.ActionContextProvider;
import docking.widgets.tree.GTree;
import docking.widgets.tree.GTreeNode;
import generic.theme.*;
import ghidra.util.WebColors;
/**
* Tree for showing colors organized by similar colors and reference relationships. This was
* built as a developer aid to help consolidate similar colors in Ghidra. This may be removed
* at any time.
*/
public class ThemeColorTree extends JPanel implements ActionContextProvider {
private ColorRootNode root;
private GTree tree;
private JComboBox<GroupingStrategy> groupingCombo;
private JComboBox<ColorSorter> colorSortCombo;
private ThemeManager themeManager;
public ThemeColorTree(ThemeManager themeManager) {
this.themeManager = themeManager;
buildBinCombo();
buildSortCombo();
root = new ColorRootNode();
tree = new GTree(root);
setLayout(new BorderLayout());
add(tree, BorderLayout.CENTER);
add(buildControls(), BorderLayout.SOUTH);
}
private Component buildControls() {
JPanel panel = new JPanel();
panel.add(new JLabel("Group By: "));
panel.add(groupingCombo);
panel.add(new JLabel("Sort Order: "));
panel.add(colorSortCombo);
return panel;
}
private Component buildBinCombo() {
groupingCombo = new JComboBox<GroupingStrategy>(GroupingStrategy.values());
groupingCombo.setSelectedItem(GroupingStrategy.BIN_64);
groupingCombo.addItemListener(this::comboChanged);
return groupingCombo;
}
private void comboChanged(ItemEvent ev) {
rebuild();
}
private Component buildSortCombo() {
ColorSorter[] sorters = { RGB, RBG, GRB, GBR, BRG, BGR };
colorSortCombo = new JComboBox<ColorSorter>(sorters);
colorSortCombo.addItemListener(this::comboChanged);
return colorSortCombo;
}
public void rebuild() {
root = new ColorRootNode();
tree.setRootNode(root);
}
private class SwatchIcon implements Icon {
private Color color;
private Color border;
SwatchIcon(Color c) {
this.color = c;
this.border = GThemeDefaults.Colors.FOREGROUND;
}
@Override
public void paintIcon(Component c, Graphics g, int x, int y) {
g.setColor(color);
g.fillRect(x, y, 16, 16);
g.setColor(border);
g.drawRect(x, y, 16, 16);
}
@Override
public int getIconWidth() {
return 18;
}
@Override
public int getIconHeight() {
return 18;
}
}
@Override
public ActionContext getActionContext(MouseEvent e) {
return null;
}
enum GroupingStrategy {
REF("Reference"),
SAME_COLORS("Same Color"),
BIN_8("8 Bins"),
BIN_64("64 Bins"),
BIN_512("512 Bins");
private String name;
GroupingStrategy(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}
private class ColorNode extends GTreeNode {
protected Color color;
protected Icon icon;
private String name;
protected String displayText;
ColorNode(Color color) {
this(color, "");
}
ColorNode(Color color, String namePrefix) {
this.name = namePrefix + getColorString(color);
this.color = color instanceof GColor ? new Color(color.getRGB()) : color;
this.icon = new SwatchIcon(color);
}
@Override
public String getName() {
return name;
}
@Override
public String getDisplayText() {
if (displayText == null) {
displayText = name;
int childCount = getChildCount();
if (childCount > 0) {
int leafCount = getLeafCount();
displayText = name + " (" + childCount + ", " + leafCount + ")";
}
}
return displayText;
}
@Override
public Icon getIcon(boolean expanded) {
return icon;
}
@Override
public String getToolTip() {
return null;
}
@Override
public boolean isLeaf() {
return false;
}
public Color getColor() {
return color;
}
protected String getColorString(Color c) {
String colorName = WebColors.toString(c, false);
String webColorName = WebColors.toWebColorName(c);
if (webColorName != null) {
colorName += " (" + webColorName + ")";
}
return colorName;
}
public void sort(ColorSorter sorter) {
if (getChildCount() == 0) {
return;
}
List<GTreeNode> children = new ArrayList<>(getChildren());
for (GTreeNode node : children) {
((ColorNode) node).sort(sorter);
}
sortChildren(children, sorter);
setChildren(children);
}
protected void sortChildren(List<GTreeNode> nodes, ColorSorter sorter) {
Collections.sort(nodes,
(a, b) -> sorter.compare(((ColorNode) a).getColor(), ((ColorNode) b).getColor()));
}
}
private class SameColorGroupNode extends ColorNode {
SameColorGroupNode(Color color) {
super(color);
}
@Override
protected void sortChildren(List<GTreeNode> nodes, ColorSorter sorter) {
Collections.sort(nodes);
}
}
private class ColorValueNode extends ColorNode {
private ColorValue colorValue;
public ColorValueNode(ColorValue colorValue) {
super(new GColor(colorValue.getId()), colorValue.getId() + " ");
this.colorValue = colorValue;
}
@Override
public boolean isLeaf() {
return getChildCount() == 0;
}
public boolean isIndirect() {
return colorValue.isIndirect();
}
public String getReferenceId() {
return colorValue.getReferenceId();
}
public String getId() {
return colorValue.getId();
}
@Override
protected void sortChildren(List<GTreeNode> nodes, ColorSorter sorter) {
Collections.sort(nodes);
}
}
private class ColorRootNode extends GTreeNode {
int uniqueColors = 0;
public ColorRootNode() {
List<GTreeNode> children = buildChildren();
setChildren(children);
uniqueColors = countUniqueColors(children);
sortChildren();
}
private void sortChildren() {
ColorSorter sorter = (ColorSorter) colorSortCombo.getSelectedItem();
List<GTreeNode> children = new ArrayList<>(getChildren());
if (children.isEmpty()) {
return;
}
for (GTreeNode node : children) {
((ColorNode) node).sort(sorter);
}
if (children.get(0) instanceof ColorValueNode) {
Collections.sort(children);
}
else {
Collections.sort(children, (a, b) -> sorter.compare(((ColorNode) a).getColor(),
((ColorNode) b).getColor()));
}
setChildren(children);
}
private int countUniqueColors(List<GTreeNode> children) {
Iterator<GTreeNode> iterator = iterator(true);
Set<Color> set = new HashSet<>();
while (iterator.hasNext()) {
GTreeNode node = iterator.next();
if (node instanceof ColorNode colorNode) {
set.add(colorNode.getColor());
}
}
return set.size();
}
private List<GTreeNode> buildChildren() {
List<ColorValueNode> nodes = new ArrayList<>();
GThemeValueMap currentValues = themeManager.getCurrentValues();
List<ColorValue> colors = currentValues.getColors();
for (ColorValue colorValue : colors) {
nodes.add(new ColorValueNode(colorValue));
}
nodes = organizeByIdRefs(nodes);
int bins = 1;
GroupingStrategy grouping = (GroupingStrategy) groupingCombo.getSelectedItem();
switch (grouping) {
case REF:
return new ArrayList<GTreeNode>(nodes);
case SAME_COLORS:
List<ColorNode> grouped = groupSameColors(nodes);
return new ArrayList<GTreeNode>(grouped);
case BIN_8:
bins = 8;
grouped = groupSameColors(nodes);
List<ColorNode> binned = binColors(grouped, bins);
return new ArrayList<GTreeNode>(binned);
case BIN_64:
bins = 64;
grouped = groupSameColors(nodes);
binned = binColors(grouped, bins);
return new ArrayList<GTreeNode>(binned);
case BIN_512:
bins = 512;
grouped = groupSameColors(nodes);
binned = binColors(grouped, bins);
return new ArrayList<GTreeNode>(binned);
default:
return new ArrayList<>();
}
}
private List<ColorValueNode> organizeByIdRefs(List<ColorValueNode> nodes) {
List<ColorValueNode> results = new ArrayList<>();
Map<String, ColorValueNode> idMap = new HashMap<>();
for (ColorValueNode node : nodes) {
idMap.put(node.getId(), node);
}
for (ColorValueNode colorNode : nodes) {
if (colorNode.isIndirect()) {
String refId = colorNode.getReferenceId();
ColorValueNode parent = idMap.get(refId);
parent.addNode(colorNode);
}
else {
results.add(colorNode);
}
}
return results;
}
private List<ColorNode> groupSameColors(List<ColorValueNode> nodes) {
Map<Color, ColorNode> colorMap = new HashMap<>();
for (ColorNode node : nodes) {
Color color = node.getColor();
ColorNode group = colorMap.computeIfAbsent(color, k -> new SameColorGroupNode(k));
group.addNode(node);
}
return new ArrayList<>(colorMap.values());
}
private List<ColorNode> binColors(List<ColorNode> nodes, int bins) {
int shift = computeShift(bins);
Map<Color, ColorNode> binnedColorMap = new HashMap<>();
for (ColorNode node : nodes) {
Color binnedColor = binColor(node, shift);
ColorNode group =
binnedColorMap.computeIfAbsent(binnedColor, k -> new ColorNode(k, "Bin "));
group.addNode(node);
}
return new ArrayList<>(binnedColorMap.values());
}
private int computeShift(int bins) {
switch (bins) {
case 8:
return 7;
case 64:
return 6;
case 512:
return 5;
default:
return 7;
}
}
private Color binColor(ColorNode node, int shift) {
Color color = node.getColor();
int redValue = (color.getRed() >> shift) << shift;
int greenValue = (color.getGreen() >> shift) << shift;
int blueValue = (color.getBlue() >> shift) << shift;
return new Color(redValue, greenValue, blueValue);
}
@Override
public String getName() {
return "Colors";
}
@Override
public String getDisplayText() {
return "Colors (" + uniqueColors + " unique colors)";
}
@Override
public Icon getIcon(boolean expanded) {
return null;
}
@Override
public String getToolTip() {
return null;
}
@Override
public boolean isLeaf() {
return false;
}
}
}

View file

@ -46,6 +46,7 @@ public class ThemeEditorDialog extends DialogComponentProvider {
private ThemeColorTable colorTable;
private ThemeFontTable fontTable;
private ThemeIconTable iconTable;
private ThemeColorTree colorTree;
private ThemeManager themeManager;
@ -148,6 +149,7 @@ public class ThemeEditorDialog extends DialogComponentProvider {
}
private void reset() {
colorTree.rebuild();
colorTable.reloadAll();
fontTable.reloadAll();
iconTable.reloadAll();
@ -173,6 +175,7 @@ public class ThemeEditorDialog extends DialogComponentProvider {
else {
setStatusText("");
}
colorTree.rebuild();
colorTable.reloadAll();
fontTable.reloadAll();
iconTable.reloadAll();
@ -229,10 +232,12 @@ public class ThemeEditorDialog extends DialogComponentProvider {
colorTable = new ThemeColorTable(themeManager, valuesCache);
iconTable = new ThemeIconTable(themeManager, valuesCache);
fontTable = new ThemeFontTable(themeManager, valuesCache);
colorTree = new ThemeColorTree(themeManager);
tabbedPane.add("Colors", colorTable);
tabbedPane.add("Fonts", fontTable);
tabbedPane.add("Icons", iconTable);
tabbedPane.add("Color Tree", colorTree);
return tabbedPane;
}
@ -287,6 +292,7 @@ public class ThemeEditorDialog extends DialogComponentProvider {
}
if (event.hasAnyColorChanged()) {
colorTable.reloadCurrent();
colorTree.rebuild();
}
if (event.hasAnyFontChanged()) {
fontTable.reloadCurrent();
@ -297,4 +303,5 @@ public class ThemeEditorDialog extends DialogComponentProvider {
updateButtons();
}
}
}