GP-1981 - Fixed table header rendering

This commit is contained in:
dragonmacher
2022-07-26 19:24:54 -04:00
committed by ghidragon
parent 5d51845cca
commit cd4ab3a156
7 changed files with 200 additions and 244 deletions
@@ -224,6 +224,10 @@ public class GHelpBroker extends DefaultHelpBroker {
private void initializeUIComponents(Container contentPane) {
if (initialized) {
return;
}
// 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();
@@ -243,6 +247,7 @@ public class GHelpBroker extends DefaultHelpBroker {
new HelpViewSearcher(jHelp, helpModel);
installActions(jHelp);
initialized = true;
}
void reload() {
@@ -22,6 +22,7 @@ import java.awt.geom.Rectangle2D;
import java.awt.image.ColorModel;
import java.util.Objects;
import ghidra.util.WebColors;
import ghidra.util.datastruct.WeakStore;
public class GColor extends Color implements Refreshable {
@@ -101,11 +102,6 @@ public class GColor extends Color implements Refreshable {
return delegate.darker();
}
@Override
public int hashCode() {
return Objects.hash(id, alpha);
}
@Override
public String toString() {
Color c = delegate;
@@ -115,6 +111,19 @@ public class GColor extends Color implements Refreshable {
c.getClass().getSimpleName() + rgb + "]";
}
/**
* Returns this color as a hex string that starts with '#'
* @return the hex string
*/
public String toHexString() {
return WebColors.toString(this, false);
}
@Override
public int hashCode() {
return Objects.hash(id, alpha);
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
@@ -38,7 +38,7 @@ public class GTableHeader extends JTableHeader {
/** This is the cursor used by BasicTableHeaderUI to tell the user they can resize a column */
private static final Cursor RESIZE_CURSOR = Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR);
private static final int HELP_ICON_HEIGHT = 8;
public static final int HELP_ICON_HEIGHT = 8;
private static final Icon HELP_ICON = ResourceManager.getScaledIcon(
ResourceManager.loadImage("images/info_small.png"), HELP_ICON_HEIGHT, HELP_ICON_HEIGHT);
private static final Icon HELP_HOVERED_ICON =
@@ -206,7 +206,7 @@ public class GTableHeader extends JTableHeader {
}
if (columnFilterToolTip != null) {
ttBuilder.append("<br><b>Filters: </b");
ttBuilder.append("<br><hr width=80%><b>Filters: </b");
ttBuilder.append(columnFilterToolTip);
}
@@ -223,12 +223,6 @@ public class GTableHeader extends JTableHeader {
if (prefWidth > cellWidth) {
return column.getHeaderValue().toString();
}
if (component instanceof GTableHeaderRenderer) {
GTableHeaderRenderer gthr = (GTableHeaderRenderer) component;
if (gthr.isTextOccluded()) {
return column.getHeaderValue().toString();
}
}
// handle the case where the user has specifically added a tooltip string
if (component instanceof JComponent) {
@@ -17,103 +17,149 @@ package docking.widgets.table;
import java.awt.*;
import java.awt.font.TextAttribute;
import java.awt.geom.Rectangle2D;
import java.text.AttributedString;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.border.Border;
import javax.swing.table.*;
import docking.theme.GColor;
import docking.widgets.label.GDLabel;
import resources.*;
import resources.icons.EmptyIcon;
import resources.icons.TranslateIcon;
/**
* The header renderer for GhidraTable.
* If the table model implements <code>SortedTableModel</code>, then
* an icon will be displayed in the header of the currently sorted
* column representing ascending and descending order.
*/
public class GTableHeaderRenderer extends JPanel implements TableCellRenderer {
public class GTableHeaderRenderer extends DefaultTableCellRenderer {
private static final Color SORT_NUMBER_FG_COLOR = new GColor("color.fg");
private static final int PADDING_FOR_COLUMN_NUMBER = 10;
//@formatter:off
public static final Color GRADIENT_START_COLOR = new GColor("color.bg.tableheader.gradient.start");
public static final Color GRADIENT_END_COLOR = new GColor("color.bg.tableheader.gradient.end");
public static final Color GRADIENT_START_PRIMARY_COLOR = new GColor("color.bg.tableheader.gradient.start.primary");
public static final Color GRADIENT_END_PRIMARY_COLOR= new GColor("color.bg.tableheader.gradient.end.primary");
//@formatter:on
// static {
// Gui.registerAltColor(GThemeDefaults.DARK, GRADIENT_START_ID, new Color(20, 20, 20));
// Gui.registerAltColor(GThemeDefaults.DARK, GRADIENT_END_ID, new Color(40, 40, 40));
// }
private static final Icon UP_ICON =
ResourceManager.getScaledIcon(Icons.SORT_ASCENDING_ICON, 14, 14);
private static final Icon DOWN_ICON =
ResourceManager.getScaledIcon(Icons.SORT_DESCENDING_ICON, 14, 14);
private static final int DEFAULT_MIN_HEIGHT = UP_ICON.getIconHeight();
private static final Icon EMPTY_ICON = new EmptyIcon(0, 0);
private static final Icon FILTER_ICON =
ResourceManager.getScaledIcon(ResourceManager.loadImage("images/filter_off.png"), 12, 12);
private JLabel textLabel = new GDLabel();
private JLabel iconLabel = new GDLabel();
private Icon helpIcon = null;
private CustomPaddingBorder customBorder;
private Icon primaryIcon = EMPTY_ICON;
private Icon helpIcon = EMPTY_ICON;
protected boolean isPaintingPrimarySortColumn;
public GTableHeaderRenderer() {
super();
private TableCellRenderer delegate;
textLabel.setHorizontalTextPosition(SwingConstants.LEFT);
iconLabel.setHorizontalAlignment(SwingConstants.RIGHT);
@Override
public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column) {
textLabel.setBorder(createOSSpecificBorder());
JTableHeader header = table.getTableHeader();
delegate = header.getDefaultRenderer();
setLayout(new BorderLayout());
add(textLabel, BorderLayout.CENTER);
add(iconLabel, BorderLayout.EAST);
Component rendererComponent =
delegate.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
// controls spacing on multiple platforms
customBorder = new CustomPaddingBorder();
setBorder(customBorder);
int modelIndex = table.convertColumnIndexToModel(column);
TableModel model = table.getModel();
VariableColumnTableModel variableModel = VariableColumnTableModel.from(model);
if (variableModel != null) {
String text = variableModel.getColumnDisplayName(modelIndex);
if (rendererComponent instanceof JLabel) {
((JLabel) rendererComponent).setText(text);
}
}
primaryIcon = getIcon(model, modelIndex);
helpIcon = getHelpIcon(table, column);
return this;
}
@Override
// overridden to paint our help icon over the other components
protected void paintChildren(Graphics g) {
super.paintChildren(g);
paintHelpIcon(g);
public void setBounds(int x, int y, int w, int h) {
super.setBounds(x, y, w, h);
((Component) delegate).setBounds(x, y, w, h);
}
private void paintHelpIcon(Graphics g) {
if (helpIcon == null) {
return;
@Override
public void paint(Graphics g) {
JLabel label = (JLabel) delegate;
String text = label.getText();
String clippedText = checkForClipping(label, text);
if (!text.equals(clippedText)) {
label.setText(clippedText);
}
Point paintPoint = getHelpIconLocation();
helpIcon.paintIcon(this, g, paintPoint.x, paintPoint.y);
label.paint(g);
// paint our items after the delegate call so that we paint on top
super.paint(g);
}
private String checkForClipping(JLabel label, String text) {
Point helpPoint = getHelpIconLocation();
int padding = 10;
int iconStartX = helpPoint.x - primaryIcon.getIconWidth() - padding;
FontMetrics metrics = label.getFontMetrics(label.getFont());
int horizontalAlignment = label.getHorizontalAlignment();
Rectangle bounds = label.getBounds();
int availableWidth = iconStartX + primaryIcon.getIconWidth();
if (horizontalAlignment == CENTER) {
availableWidth = iconStartX - padding;
}
String clippedText = SwingUtilities.layoutCompoundLabel(
label,
metrics,
text,
primaryIcon,
label.getVerticalAlignment(),
label.getHorizontalAlignment(),
label.getVerticalTextPosition(),
label.getHorizontalTextPosition(),
new Rectangle(0, 0, availableWidth, bounds.height),
new Rectangle(iconStartX, 0, primaryIcon.getIconWidth(), bounds.height),
new Rectangle(0, 0, iconStartX, bounds.height),
label.getIconTextGap());
return clippedText;
}
@Override
protected void paintChildren(Graphics g) {
// The help icon paints at the end of the cell; place the main icon to the left of that
Point helpPoint = getHelpIconLocation();
int offset = 4;
int x = helpPoint.x - primaryIcon.getIconWidth() - offset;
int y = getIconStartY(primaryIcon.getIconHeight());
primaryIcon.paintIcon(this, g, x, y);
helpIcon.paintIcon(this, g, helpPoint.x, helpPoint.y);
}
private Point getHelpIconLocation() {
int right = getWidth();
int offset = 2;
int helpIconWidth = GTableHeader.HELP_ICON_HEIGHT;
// we want the icon on the right-hand size of the header, at the top
int primaryWidth = iconLabel.getWidth();
int overlayWidth = helpIcon.getIconWidth();
// this point is relative to the iconLabel...
Point paintPoint = new Point(primaryWidth - overlayWidth, 0);
// ...make the point relative to the parent (this renderer)
return SwingUtilities.convertPoint(iconLabel, paintPoint, this);
int x = right - helpIconWidth - offset;
int y = offset; // down a bit
return new Point(x, y);
}
@Override
// overridden to enforce a minimum height for the icon we use
public Dimension getPreferredSize() {
Dimension preferredSize = super.getPreferredSize();
if (delegate != null) {
return ((Component) delegate).getPreferredSize();
}
Border currentBorder = getBorder();
int minHeight = DEFAULT_MIN_HEIGHT;
if (currentBorder != null) {
@@ -124,47 +170,19 @@ public class GTableHeaderRenderer extends JPanel implements TableCellRenderer {
return preferredSize;
}
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
boolean hasFocus, int row, int column) {
isPaintingPrimarySortColumn = false; // reset
Icon icon = null;
String text = (value == null) ? "" : value.toString();
JTableHeader header = table.getTableHeader();
setForeground(header.getForeground());
setFont(header.getFont());
// remap the column index to the models column index
int modelIndex = table.convertColumnIndexToModel(column);
TableModel model = table.getModel();
icon = getIcon(model, modelIndex);
VariableColumnTableModel variableModel = VariableColumnTableModel.from(model);
if (variableModel != null) {
text = variableModel.getColumnDisplayName(modelIndex);
}
updateHelpIcon(table, column, icon);
iconLabel.setIcon(icon);
textLabel.setText(text);
setOuterBorder(customBorder, column);
setOpaque(false);
return this;
}
private Icon getIcon(TableModel model, int columnModelIndex) {
Icon icon = null;
if (model instanceof SortedTableModel) {
icon = getSortIcon(icon, columnModelIndex, model);
}
if (isColumnFiltered(model, columnModelIndex)) {
icon = combineIcons(icon, FILTER_ICON);
icon = combineIcons(FILTER_ICON, icon);
}
return icon;
if (icon != null) {
return icon;
}
return EMPTY_ICON;
}
private Icon combineIcons(Icon icon1, Icon icon2) {
@@ -174,9 +192,16 @@ public class GTableHeaderRenderer extends JPanel implements TableCellRenderer {
if (icon2 == null) {
return icon1;
}
MultiIcon icon = new MultiIcon(new EmptyIcon(28, 14));
icon.addIcon(icon2);
icon.addIcon(new TranslateIcon(icon1, 14, 0));
int padding = 2;
int w1 = icon1.getIconWidth();
int w2 = icon2.getIconWidth();
int h1 = icon1.getIconHeight();
int fullWidth = w1 + padding + w2;
MultiIcon icon = new MultiIcon(new EmptyIcon(fullWidth, h1));
icon.addIcon(icon1);
int rightShift = w1 + padding;
icon.addIcon(new TranslateIcon(icon2, rightShift, 0));
return icon;
}
@@ -192,66 +217,26 @@ public class GTableHeaderRenderer extends JPanel implements TableCellRenderer {
return tableFilter.hasColumnFilter(columnModelIndex);
}
private void setOuterBorder(CustomPaddingBorder border, int column) {
if (paintAquaHeaders()) {
if (column == 0) {
customBorder.setOuterBorder(new NoSidesLineBorder(Color.GRAY));
return;
}
customBorder.setOuterBorder(new NoRightSideLineBorder(Color.GRAY));
}
else {
customBorder.setOuterBorder(UIManager.getBorder("TableHeader.cellBorder"));
}
}
private Icon getHelpIcon(JTable table, int currentColumnIndex) {
private boolean paintAquaHeaders() {
return true;
// For now we always use the custom --it actually makes the various LaFs look nicer
// return DockingWindowsLookAndFeelUtils.isUsingAquaUI(getUI());
}
@Override
protected void paintComponent(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
Paint backgroundColor = getBackgroundPaint();
Paint oldPaint = g2d.getPaint();
g2d.setPaint(backgroundColor);
g2d.fillRect(0, 0, getWidth(), getHeight());
g2d.setPaint(oldPaint);
super.paintComponent(g);
}
protected Paint getBackgroundPaint() {
Color startColor =
isPaintingPrimarySortColumn ? GRADIENT_START_PRIMARY_COLOR : GRADIENT_START_COLOR;
Color endColor =
isPaintingPrimarySortColumn ? GRADIENT_END_PRIMARY_COLOR : GRADIENT_END_COLOR;
return new GradientPaint(0, 0, startColor, 0, getHeight() - 11, endColor, true);
}
private void updateHelpIcon(JTable table, int currentColumnIndex, Icon icon) {
JTableHeader tableHeader = table.getTableHeader();
if (!(tableHeader instanceof GTableHeader)) {
helpIcon = null;
return;
return EMPTY_ICON;
}
GTableHeader tooltipTableHeader = (GTableHeader) tableHeader;
int hoveredColumnIndex = tooltipTableHeader.getHoveredHeaderColumnIndex();
if (hoveredColumnIndex != currentColumnIndex) {
helpIcon = null;
return;
return EMPTY_ICON;
}
helpIcon = tooltipTableHeader.getHelpIcon();
Icon icon = tooltipTableHeader.getHelpIcon();
if (icon != null) {
return icon;
}
return EMPTY_ICON;
}
// checked before this method is called
private Icon getSortIcon(Icon icon, int realIndex, TableModel model) {
SortedTableModel sortedModel = (SortedTableModel) model;
TableSortState columnSortStates = sortedModel.getTableSortState();
@@ -311,75 +296,36 @@ public class GTableHeaderRenderer extends JPanel implements TableCellRenderer {
return icon;
}
/**
* Returns true if sorted in ascending order, false if descending.
* @return true if sorted in ascending order, false if descending
*/
public boolean isSortedAscending() {
return iconLabel.getIcon() == UP_ICON;
}
private int getIconStartY(int iconHeight) {
boolean isTextOccluded() {
return textLabel.getPreferredSize().getWidth() > textLabel.getWidth();
}
private Border createOSSpecificBorder() {
if (paintAquaHeaders()) {
return new EmptyBorder(1, 2, 1, 2);
}
return new EmptyBorder(0, 2, 0, 2);
int height = getHeight();
int middle = height / 2;
int halfHeight = iconHeight / 2;
int y = middle - halfHeight;
return y;
}
//==================================================================================================
// Inner Classes
//==================================================================================================
private class CustomPaddingBorder extends CompoundBorder {
private CustomPaddingBorder() {
insideBorder = createOSSpecificBorder();
}
void setOuterBorder(Border border) {
outsideBorder = border;
}
}
private class NoRightSideLineBorder extends LineBorder {
NoRightSideLineBorder(Color color) {
super(color);
}
@Override
public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
// take advantage of our clipping by telling our parent to paint at a point that will
// be clipped
super.paintBorder(c, g, x, y, width + 1, height);
}
}
private class NoSidesLineBorder extends LineBorder {
NoSidesLineBorder(Color color) {
super(color);
}
@Override
public void paintBorder(Component c, Graphics g, int x, int y, int width, int height) {
// take advantage of our clipping by telling our parent to paint at a point that will
// be clipped
super.paintBorder(c, g, x - 1, y, width + 5, height);
}
}
private class NumberPainterIcon implements Icon {
private final int iconWidth;
private int numberWidth;
private final int iconHeight;
private final String numberText;
public NumberPainterIcon(int width, int height, String numberText) {
iconWidth = width;
iconHeight = height;
this.iconWidth = width;
this.iconHeight = height;
this.numberText = numberText;
int fontSize = 12;
String fontFamily = "arial";
Font font = new Font(fontFamily, Font.BOLD, fontSize);
FontMetrics fontMetrics = getFontMetrics(font);
numberWidth = fontMetrics.stringWidth(numberText);
}
@Override
@@ -389,35 +335,36 @@ public class GTableHeaderRenderer extends JPanel implements TableCellRenderer {
@Override
public int getIconWidth() {
return iconWidth;
return iconWidth + numberWidth;
}
@Override
public void paintIcon(Component c, Graphics g, int x, int y) {
int fontSize = 12;
String fontFamily = "arial";
Font font = new Font(fontFamily, Font.BOLD, fontSize);
g.setFont(font);
FontMetrics fontMetrics = g.getFontMetrics();
Rectangle2D stringBounds = fontMetrics.getStringBounds(numberText, g);
int numberWidth = (int) stringBounds.getWidth();
int numberHeight = fontMetrics.getAscent();
int insetPadding = 2;
int padding = 2;
// draw the number on the right...
int startX = x + (iconWidth - numberWidth) - insetPadding;
int startX = x + (iconWidth - numberWidth) + padding;
// ...and in the upper portion
int textBaseline = numberHeight;
// ...and at the same start y as the sort icon
int iconY = getIconStartY(iconHeight);
int textBaseline = iconY + numberHeight - padding;
AttributedString as = new AttributedString(numberText);
as.addAttribute(TextAttribute.FOREGROUND, Color.BLACK);
as.addAttribute(TextAttribute.FOREGROUND, SORT_NUMBER_FG_COLOR);
as.addAttribute(TextAttribute.WEIGHT, TextAttribute.WEIGHT_BOLD);
as.addAttribute(TextAttribute.FAMILY, fontFamily);
as.addAttribute(TextAttribute.SIZE, (float) fontSize);
g.drawString(as.getIterator(), startX, textBaseline);
}
}
@@ -20,6 +20,8 @@ import java.util.stream.Collectors;
import org.jdom.Element;
import docking.theme.GColor;
import docking.theme.GThemeDefaults.Colors.Palette;
import docking.widgets.table.*;
import docking.widgets.table.constraint.ColumnConstraint;
import docking.widgets.table.constraint.TableFilterContext;
@@ -230,16 +232,19 @@ public class ColumnBasedTableFilter<R> implements TableFilter<R> {
//
private String getHtmlRepresentation(List<ColumnConstraintSet<R, ?>> filters) {
StringBuilder buf = new StringBuilder();
buf.append("<table valign=top cellspacing=5 cellpadding=0 >");
buf.append("<table valign=top cellspacing=5 cellpadding=0>");
buf.append("<tr>");
// The first row has an empty first column
// so that additional rows can display an "AND"
buf.append("<td></td><td>");
buf.append(filters.get(0).getHtmlRepresentation());
buf.append("</td></tr>");
GColor gray = Palette.GRAY;
String grayHex = gray.toHexString();
for (int i = 1; i < filters.size(); i++) {
buf.append("<tr><td style=\"color:gray\"> " + filters.get(i).getLogicOperation() +
"&nbsp;</td><td>");
buf.append("<tr><td style=\"color:").append(grayHex).append("\"> ");
buf.append(filters.get(i).getLogicOperation());
buf.append("&nbsp;</td><td>");
buf.append(filters.get(i).getHtmlRepresentation());
buf.append("</td></tr>");
}
@@ -19,6 +19,8 @@ import java.util.*;
import org.apache.commons.collections4.CollectionUtils;
import docking.theme.GColor;
import docking.theme.GThemeDefaults.Colors.Palette;
import docking.widgets.table.DiscoverableTableUtils;
import docking.widgets.table.RowObjectTableModel;
import docking.widgets.table.constraint.*;
@@ -178,25 +180,30 @@ public class ColumnConstraintSet<R, T> {
* Returns an HTML representation of this constraint set in a tabular form. It will be used
* inside the HTML representation of the entire filter. See {@link ColumnBasedTableFilter#getHtmlRepresentation()}
* for a description of the table format.
* @return the html
*/
String getHtmlRepresentation() {
StringBuilder builder = new StringBuilder();
builder.append("<table valign=top cellpadding=0 cellspacing=0>");
builder.append("<tr><td style=\"color:#990099\">");
builder.append(model.getColumnName(columnIndex));
builder.append("&nbsp;");
builder.append("</td>");
builder.append("<td>");
builder.append(getHtmlRepresentation(constraints.get(0)));
builder.append("<td></tr>");
StringBuilder buf = new StringBuilder();
buf.append("<table valign=top cellpadding=0 cellspacing=0>");
buf.append("<tr><td style=\"font-weight:bold\">");
buf.append(model.getColumnName(columnIndex));
buf.append("&nbsp;");
buf.append("</td>");
buf.append("<td>");
buf.append(getHtmlRepresentation(constraints.get(0)));
buf.append("<td></tr>");
GColor gray = Palette.GRAY;
String grayHex = gray.toHexString();
for (int i = 1; i < constraints.size(); i++) {
builder.append("<tr><td style=\"color:gray;text-align:center\">or</td>");
builder.append("<td >");
builder.append(getHtmlRepresentation(constraints.get(i)));
builder.append("</td></tr>");
buf.append("<tr><td style=\"color:").append(grayHex);
buf.append("; text-align:center\">or</td>");
buf.append("<td >");
buf.append(getHtmlRepresentation(constraints.get(i)));
buf.append("</td></tr>");
}
builder.append("</table>");
return builder.toString();
buf.append("</table>");
return buf.toString();
}
private String getHtmlRepresentation(ColumnConstraint<?> columnConstraint) {
@@ -209,7 +216,7 @@ public class ColumnConstraintSet<R, T> {
if (quoteValue) {
buf.append("\"");
}
buf.append("<span style=\"color: blue\">");
buf.append("<span style=\"font-weight:bold\">");
buf.append(columnConstraint.getConstraintValueString());
buf.append("</span>");
if (quoteValue) {
@@ -141,17 +141,6 @@ public class HTMLUtilities {
public static String HTML_SPACE = "&nbsp;";
public static String HTML_NEW_LINE = BR;
public static final String MAROON = "#990000";
public static final String GREEN = "#009900";
public static final String BLUE = "#000099";
public static final String PURPLE = "#990099";
public static final String DARK_CYAN = "#009999";
public static final String OLIVE = "#999900";
public static final String ORANGE = "#FF9900";
public static final String PINK = "#FF9999";
public static final String YELLOW = "#FFFF00";
public static final String GRAY = "#888888";
/**
* Marks the given text as HTML in order to be rendered thusly by Java widgets.
*