Merge remote-tracking branch

'origin/GP-5481_ghidragon_data_graph--SQUASHED'
This commit is contained in:
Ryan Kurtz 2025-07-03 06:25:34 -04:00
commit 3e50533187
102 changed files with 9268 additions and 366 deletions

View file

@ -1,120 +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;
import java.awt.*;
import java.awt.geom.GeneralPath;
import javax.swing.Icon;
/**
* Icon for a close button
*/
public class CloseIcon implements Icon {
private int size;
private Color color;
private Shape shape;
/**
* Creates a close icon.
* @param isSmall false signals to use a 16x16 size; true signals to use an 8x8 size
* @param color the color of the "x"
*/
public CloseIcon(boolean isSmall, Color color) {
this.size = isSmall ? 8 : 16;
this.color = color;
this.shape = buildShape();
}
private Shape buildShape() {
GeneralPath path = new GeneralPath();
/*
We use trial and error sizing. This class allows clients to specify the icon size. At
the time of writing, there were only 2 sizes in use: 16 and 8 pixels. If more size
needs arise, we can revisit how the values below are chosen.
*/
double margin = 2;
double shapeSize = 11;
double thickness = 1.7;
if (size == 8) {
margin = 0;
shapeSize = 7;
thickness = 1;
}
double p1x = margin;
double p1y = margin + thickness;
double p2x = margin + thickness;
double p2y = margin;
double p3x = margin + shapeSize;
double p3y = margin + shapeSize - thickness;
double p4x = margin + shapeSize - thickness;
double p4y = margin + shapeSize;
path.moveTo(p1x, p1y);
path.lineTo(p2x, p2y);
path.lineTo(p3x, p3y);
path.lineTo(p4x, p4y);
path.lineTo(p1x, p1y);
p1x = margin + shapeSize - thickness;
p1y = margin;
p2x = margin + shapeSize;
p2y = margin + thickness;
p3x = margin + thickness;
p3y = margin + shapeSize;
p4x = margin;
p4y = margin + shapeSize - thickness;
path.moveTo(p1x, p1y);
path.lineTo(p2x, p2y);
path.lineTo(p3x, p3y);
path.lineTo(p4x, p4y);
path.lineTo(p1x, p1y);
path.closePath();
return path;
}
@Override
public void paintIcon(Component c, Graphics g, int x, int y) {
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
try {
g2d.translate(x, y);
g2d.setColor(color);
g2d.fill(shape);
}
finally {
g2d.translate(-x, -y);
}
}
@Override
public int getIconWidth() {
return size;
}
@Override
public int getIconHeight() {
return size;
}
}

View file

@ -147,6 +147,7 @@ public class DockableComponent extends JPanel implements ContainerListener {
}
private void showContextMenu(MouseEvent e) {
if (e.isConsumed()) {
return;
}

View file

@ -23,6 +23,7 @@ import javax.swing.*;
import docking.action.*;
import docking.menu.*;
import generic.theme.CloseIcon;
import generic.theme.GColor;
import ghidra.util.exception.AssertException;
import ghidra.util.task.SwingUpdateManager;
@ -32,7 +33,7 @@ import ghidra.util.task.SwingUpdateManager;
*/
class DockableToolBarManager {
private static final Color BUTTON_COLOR = new GColor("color.fg.button");
private static final Icon CLOSE_ICON = new CloseIcon(false, BUTTON_COLOR);
private static final Icon CLOSE_ICON = new CloseIcon(false);
private Icon MENU_ICON = new DropDownMenuIcon(BUTTON_COLOR);
private GenericHeader dockableHeader;
private ToolBarManager toolBarManager;

View file

@ -4,9 +4,9 @@
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
*
* http://www.apache.org/licenses/LICENSE-2.0
*
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@ -277,6 +277,10 @@ public class GenericHeader extends JPanel {
titlePanel.setTitle(title);
}
public String getTitle() {
return titlePanel.getTitle();
}
public void setIcon(Icon icon) {
titlePanel.setIcon(icon);
}
@ -396,6 +400,10 @@ public class GenericHeader extends JPanel {
titleLabel.setToolTipText(s);
}
String getTitle() {
return titleLabel.getText();
}
void setIcon(Icon icon) {
icon = DockingUtils.scaleIconAsNeeded(icon);

View file

@ -20,10 +20,9 @@ import java.awt.event.*;
import javax.swing.*;
import docking.CloseIcon;
import docking.widgets.EmptyBorderButton;
import docking.widgets.label.GDLabel;
import generic.theme.GColor;
import generic.theme.CloseIcon;
/**
* A widget that can be used to render an icon, title and close button for JTabbedPane. You would
@ -32,7 +31,7 @@ import generic.theme.GColor;
public class DockingTabRenderer extends JPanel {
private static final int MAX_TITLE_LENGTH = 25;
private Icon CLOSE_ICON = new CloseIcon(true, new GColor("color.fg.button"));
private Icon CLOSE_ICON = new CloseIcon(true);
private JLabel titleLabel;
private JLabel iconLabel;

View file

@ -0,0 +1,45 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking.widgets.trable;
import java.util.ArrayList;
import java.util.List;
/**
* Abstract base class for GTrable models. Adds support for listeners.
*
* @param <T> the row data object type
*/
public abstract class AbstractGTrableRowModel<T> implements GTrableRowModel<T> {
private List<GTrableModeRowlListener> listeners = new ArrayList<>();
@Override
public void addListener(GTrableModeRowlListener l) {
listeners.add(l);
}
@Override
public void removeListener(GTrableModeRowlListener l) {
listeners.remove(l);
}
protected void fireModelChanged() {
for (GTrableModeRowlListener listener : listeners) {
listener.trableChanged();
}
}
}

View file

@ -0,0 +1,51 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking.widgets.trable;
import java.awt.Color;
import java.awt.Component;
import javax.swing.table.DefaultTableCellRenderer;
/**
* Base class for GTrable cell renderers.
*
* @param <T> the data model row object type
*/
public class DefaultGTrableCellRenderer<T> extends DefaultTableCellRenderer
implements GTrableCellRenderer<T> {
@Override
public Component getCellRenderer(GTrable<?> trable, T value, boolean isSelected,
boolean hasFocus, int row, int column) {
if (trable == null) {
return this;
}
Color fg = isSelected ? trable.getSelectionForeground() : trable.getForeground();
Color bg = isSelected ? trable.getSelectionBackground() : trable.getBackground();
super.setForeground(fg);
super.setBackground(bg);
setFont(trable.getFont());
setValue(value);
return this;
}
}

View file

@ -0,0 +1,98 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking.widgets.trable;
import java.util.ArrayList;
import java.util.List;
/**
* Default implementation for a simple {@link GTrable} row data model.
*
* @param <T> the row object type
*/
public class DefaultGTrableRowModel<T extends GTrableRow<T>> extends AbstractGTrableRowModel<T> {
protected List<T> rows;
public DefaultGTrableRowModel(List<T> roots) {
this.rows = new ArrayList<>(roots);
}
@Override
public int getRowCount() {
return rows.size();
}
@Override
public T getRow(int index) {
return rows.get(index);
}
@Override
public int getIndentLevel(int rowIndex) {
return rows.get(rowIndex).getIndentLevel();
}
@Override
public boolean isExpanded(int rowIndex) {
return rows.get(rowIndex).isExpanded();
}
@Override
public boolean isExpandable(int rowIndex) {
return rows.get(rowIndex).isExpandable();
}
@Override
public int collapseRow(int lineIndex) {
T row = rows.get(lineIndex);
int indentLevel = row.getIndentLevel();
int removedCount = removeIndentedRows(lineIndex + 1, indentLevel + 1);
row.setExpanded(false);
fireModelChanged();
return removedCount;
}
protected int removeIndentedRows(int startIndex, int indentLevel) {
int endIndex = findNextIndexAtLowerIndentLevel(startIndex, indentLevel);
rows.subList(startIndex, endIndex).clear();
return endIndex - startIndex;
}
protected int findNextIndexAtLowerIndentLevel(int startIndex, int indentLevel) {
for (int i = startIndex; i < rows.size(); i++) {
T line = rows.get(i);
if (line.getIndentLevel() < indentLevel) {
return i;
}
}
return rows.size();
}
@Override
public int expandRow(int lineIndex) {
T row = rows.get(lineIndex);
if (!row.isExpandable() || row.isExpanded()) {
return 0;
}
List<T> children = row.getChildRows();
rows.addAll(lineIndex + 1, children);
row.setExpanded(true);
fireModelChanged();
return children.size();
}
}

View file

@ -0,0 +1,646 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking.widgets.trable;
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import javax.swing.*;
import docking.DockingUtils;
import ghidra.util.datastruct.Range;
/**
* Component that combines the display of a tree and a table. Data is presented in columns like a
* table, but rows can have child rows like a tree which are displayed indented in the first
* column.
* <P>
* A GTrable uses two different models: a row model and a column model. The row model contains
* row objects that contains the data to be displayed on a given row. The column model specifies
* how to display the data in the row object as a series of column values.
* <P>
* The row model also provides information about the parent child relationship of rows. If the
* model reports that a row can be expanded, an expand control is show on that row. If the row
* is then expanded, the model will then report additional rows immediately below the parent row,
* pushing any existing rows further down (i.e. all rows below the row being opened have their row
* indexes increased by the number of rows added.)
*
* @param <T> The row object type
*/
public class GTrable<T> extends JComponent
implements Scrollable, GTrableModeRowlListener {
private static final int ICON_WIDTH = 16;
private static final int INDENT_WIDTH = 12;
private static final int DEFAULT_MAX_VISIBLE_ROWS = 10;
private static final int DEFAULT_MIN_VISIBLE_ROWS = 10;
private static OpenCloseIcon OPEN_ICON = new OpenCloseIcon(true, ICON_WIDTH, ICON_WIDTH);
private static OpenCloseIcon CLOSED_ICON = new OpenCloseIcon(false, ICON_WIDTH, ICON_WIDTH);
private Color selectionForground = UIManager.getColor("List.selectionForeground");
private Color selectionBackground = UIManager.getColor("List.selectionBackground");
private int minVisibleRows = DEFAULT_MIN_VISIBLE_ROWS;
private int maxVisibleRows = DEFAULT_MAX_VISIBLE_ROWS;
private int rowHeight = 20;
private GTrableRowModel<T> rowModel;
private GTrableColumnModel<T> columnModel;
private CellRendererPane renderPane;
private int selectedRow = -1;
private List<GTrableCellClickedListener> cellClickedListeners = new ArrayList<>();
private List<Consumer<Integer>> selectedRowConsumers = new ArrayList<>();
/**
* Constructor
* @param rowModel the model that provides the row data.
* @param columnModel the model the provides the column information for displaying the data
* stored in the row data.
*/
public GTrable(GTrableRowModel<T> rowModel, GTrableColumnModel<T> columnModel) {
this.rowModel = rowModel;
this.columnModel = columnModel;
this.rowModel.addListener(this);
renderPane = new CellRendererPane();
add(renderPane);
GTrableMouseListener l = new GTrableMouseListener();
addMouseListener(l);
addMouseMotionListener(l);
addKeyListener(new GTrableKeyListener());
setFocusable(true);
}
/**
* Sets a new row model.
* @param newRowModel the new row model to use
*/
public void setRowModel(GTrableRowModel<T> newRowModel) {
rowModel.removeListener(this);
rowModel = newRowModel;
newRowModel.addListener(this);
}
/**
* Sets a new column model.
* @param columnModel the new column model to use
*/
public void setColumnModel(GTrableColumnModel<T> columnModel) {
this.columnModel = columnModel;
}
/**
* Sets the preferred number of visible rows to be displayed in the scrollable area.
* @param minVisibleRows the minimum number of visible rows.
* @param maxVisibleRows the maximum number of visible rows.
*/
public void setPreferredVisibleRowCount(int minVisibleRows, int maxVisibleRows) {
this.minVisibleRows = minVisibleRows;
this.maxVisibleRows = maxVisibleRows;
}
/**
* Adds a listener to be notified if the user clicks on a cell in the GTrable.
* @param listener the listener to be notified
*/
public void addCellClickedListener(GTrableCellClickedListener listener) {
cellClickedListeners.add(listener);
}
/**
* Removes a cell clicked listener.
* @param listener the listener to be removed
*/
public void removeCellClickedListener(GTrableCellClickedListener listener) {
cellClickedListeners.remove(listener);
}
/**
* Adds a consumer to be notified when the selected row changes.
* @param consumer the consumer to be notified when the selected row changes
*/
public void addSelectedRowConsumer(Consumer<Integer> consumer) {
selectedRowConsumers.add(consumer);
}
/**
* Removes the consumer to be notified when the selected row changes.
* @param consumer the consumer to be removed
*/
public void removeSelectedRowConsumer(Consumer<Integer> consumer) {
selectedRowConsumers.remove(consumer);
}
@Override
public Dimension getPreferredSize() {
return new Dimension(columnModel.getPreferredWidth(), rowModel.getRowCount() * rowHeight);
}
@Override
public void paint(Graphics g) {
Rectangle clipBounds = g.getClipBounds();
int startIndex = getStartIndex(clipBounds);
int endIndex = getEndIndex(clipBounds);
for (int index = startIndex; index <= endIndex; index++) {
drawRow(g, index);
}
}
/**
* {@return the range of visible row indices.}
*/
public Range getVisibleRows() {
Container parent = getParent();
Rectangle rect;
if (parent instanceof JViewport viewport) {
rect = viewport.getViewRect();
}
else {
rect = getVisibleRect();
}
return new Range(getStartIndex(rect), getEndIndex(rect));
}
/**
* {@return the currently selected row or -1 if not row is selected.}
*/
public int getSelectedRow() {
return selectedRow;
}
/**
* Sets the selected row to the given row index
* @param rowIndex the row index to select
*/
public void setSelectedRow(int rowIndex) {
if (rowIndex >= 0 && rowIndex < rowModel.getRowCount()) {
this.selectedRow = rowIndex;
repaint();
notifySelectedRowConsumers();
}
}
/**
* Deselects any selected row
*/
public void clearSelectedRow() {
this.selectedRow = -1;
repaint();
}
/**
* {@return the selection foreground color}
*/
public Color getSelectionForeground() {
return selectionForground;
}
/**
* {@return the selection background color}
*/
public Color getSelectionBackground() {
return selectionBackground;
}
/**
* {@return the height of a row in the trable.}
*/
public int getRowHeight() {
return rowHeight;
}
/**
* {@return the amount the view is scrolled such that the first line is not fully visible.}
*/
public int getRowOffcut() {
Rectangle visibleRect = getVisibleRect();
int y = visibleRect.y;
return y % rowHeight;
}
@Override
public Dimension getPreferredScrollableViewportSize() {
int size = Math.min(rowModel.getRowCount(), maxVisibleRows);
size = Math.max(size, minVisibleRows);
return new Dimension(columnModel.getPreferredWidth(), size * rowHeight);
}
@Override
public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation,
int direction) {
return 5;
}
@Override
public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation,
int direction) {
return 50;
}
@Override
public boolean getScrollableTracksViewportWidth() {
return true;
}
@Override
public boolean getScrollableTracksViewportHeight() {
return false;
}
/**
* Expands the row at the given index.
* @param rowIndex the index of the row to expand
*/
public void expandRow(int rowIndex) {
int numRowsAdded = rowModel.expandRow(rowIndex);
if (selectedRow > rowIndex) {
setSelectedRow(selectedRow + numRowsAdded);
}
}
/**
* Collapse the row (remove any of its descendants) at the given row index.
* @param rowIndex the index of the row to collapse
*/
public void collapseRow(int rowIndex) {
int numRowsDeleted = rowModel.collapseRow(rowIndex);
if (selectedRow > rowIndex) {
int newSelectedRow = selectedRow - numRowsDeleted;
if (newSelectedRow < rowIndex) {
newSelectedRow = rowIndex;
}
setSelectedRow(newSelectedRow);
}
}
/**
* Fully expands the given row and all its descendants.
* @param rowIndex the index of the row to fully expand
*/
public void expandRowRecursively(int rowIndex) {
int startIndentLevel = rowModel.getIndentLevel(rowIndex);
int numRowsAdded = rowModel.expandRow(rowIndex);
if (selectedRow > rowIndex) {
setSelectedRow(selectedRow + numRowsAdded);
}
int nextRow = rowIndex + 1;
while (nextRow < rowModel.getRowCount() &&
rowModel.getIndentLevel(nextRow) > startIndentLevel) {
numRowsAdded = rowModel.expandRow(nextRow);
if (selectedRow > nextRow) {
setSelectedRow(selectedRow + numRowsAdded);
}
nextRow++;
}
}
/**
* Expands all rows fully.
*/
public void expandAll() {
int rowIndex = 0;
for (rowIndex = 0; rowIndex < rowModel.getRowCount(); rowIndex++) {
int indentLevel = rowModel.getIndentLevel(rowIndex);
if (indentLevel == 0) {
expandRowRecursively(rowIndex);
}
}
}
/**
* Collapses all rows.
*/
public void collapseAll() {
int rowIndex = 0;
for (rowIndex = 0; rowIndex < rowModel.getRowCount(); rowIndex++) {
int indentLevel = rowModel.getIndentLevel(rowIndex);
if (indentLevel == 0) {
collapseRow(rowIndex);
}
}
}
/**
* Scrolls the view to make the currently selected row visible.
*/
public void scrollToSelectedRow() {
if (selectedRow < 0) {
return;
}
Container parent = getParent();
if (!(parent instanceof JViewport viewport)) {
return;
}
Rectangle viewRect = viewport.getViewRect();
int yStart = selectedRow * rowHeight;
int yEnd = yStart + rowHeight;
if (yStart < viewRect.y) {
viewport.setViewPosition(new Point(0, yStart));
}
else if (yEnd > viewRect.y + viewRect.height) {
viewport.setViewPosition(new Point(0, yEnd - viewRect.height));
}
}
@Override
public void trableChanged() {
setSize(getWidth(), rowModel.getRowCount() * rowHeight);
revalidate();
repaint();
}
@Override
public void setBounds(int x, int y, int width, int height) {
super.setBounds(x, y, width, height);
columnModel.setWidth(width);
}
private void notifySelectedRowConsumers() {
for (Consumer<Integer> consumer : selectedRowConsumers) {
consumer.accept(selectedRow);
}
}
private void notifyCellClicked(int row, int column, MouseEvent e) {
for (GTrableCellClickedListener listener : cellClickedListeners) {
listener.cellClicked(row, column, e);
}
}
private void drawRow(Graphics g, int rowIndex) {
T row = rowModel.getRow(rowIndex);
int width = getWidth();
boolean isSelected = rowIndex == selectedRow;
int y = rowIndex * rowHeight;
Color fg = isSelected ? selectionForground : getForeground();
Color bg = isSelected ? selectionBackground : getBackground();
g.setColor(bg);
g.fillRect(0, y, width, rowHeight);
GTrableColumn<T, ?> firstColumn = columnModel.getColumn(0);
int colWidth = firstColumn.getWidth();
int marginWidth = paintLeftMargin(g, rowIndex, y, colWidth, fg);
int x = marginWidth;
paintColumn(g, x, y, colWidth - marginWidth, firstColumn, row, isSelected);
x = colWidth;
for (int i = 1; i < columnModel.getColumnCount(); i++) {
GTrableColumn<T, ?> column = columnModel.getColumn(i);
colWidth = column.getWidth();
paintColumn(g, x, y, colWidth, column, row, isSelected);
x += colWidth;
}
}
private <C> void paintColumn(Graphics g, int x, int y, int width, GTrableColumn<T, C> column,
T row, boolean isSelected) {
GTrableCellRenderer<C> renderer = column.getRenderer();
C columnValue = column.getValue(row);
Component component = renderer.getCellRenderer(this, columnValue, isSelected, false, 0, 0);
renderPane.paintComponent(g, component, this, x, y, width, rowHeight);
}
private int paintLeftMargin(Graphics g, int rowIndex, int y, int width, Color fg) {
int x = rowModel.getIndentLevel(rowIndex) * INDENT_WIDTH;
drawOpenCloseControl(g, rowIndex, x, y, fg);
return x + ICON_WIDTH;
}
private void drawOpenCloseControl(Graphics g, int rowIndex, int x, int y, Color fg) {
if (!rowModel.isExpandable(rowIndex)) {
return;
}
OpenCloseIcon icon = rowModel.isExpanded(rowIndex) ? OPEN_ICON : CLOSED_ICON;
icon.setColor(fg);
icon.paintIcon(this, g, x, y + rowHeight / 2 - icon.getIconHeight() / 2);
}
private int getStartIndex(Rectangle clipBounds) {
if (clipBounds.height == 0) {
return 0;
}
int index = clipBounds.y / rowHeight;
return Math.min(index, rowModel.getRowCount() - 1);
}
private int getEndIndex(Rectangle clipBounds) {
if (clipBounds.height == 0) {
return 0;
}
int y = clipBounds.y + clipBounds.height - 1;
return Math.min(y / rowHeight, rowModel.getRowCount() - 1);
}
private void toggleOpen(int rowIndex) {
if (rowIndex < 0) {
return;
}
if (!rowModel.isExpandable(rowIndex)) {
return;
}
if (rowModel.isExpanded(rowIndex)) {
collapseRow(rowIndex);
}
else {
expandRow(rowIndex);
}
}
private class GTrableMouseListener extends MouseAdapter {
private static final int TRIGGER_MARGIN = 10;
private int startDragX = -1;
private int originalColumnStart;
private int boundaryIndex;
@Override
public void mouseClicked(MouseEvent e) {
if (e.getButton() != 1) {
return;
}
Point point = e.getPoint();
int rowIndex = point.y / rowHeight;
if (isOnOpenClose(rowIndex, point.x)) {
toggleOpen(rowIndex);
}
else {
int columnIndex = getColumnIndex(rowIndex, point.x);
if (columnIndex >= 0) {
notifyCellClicked(rowIndex, columnIndex, e);
}
}
}
private int getColumnIndex(int rowIndex, int x) {
int columnIndex = columnModel.getIndex(x);
if (columnIndex == 0) {
int indent = rowModel.getIndentLevel(rowIndex) * INDENT_WIDTH;
if (x < indent) {
return -1;
}
}
return columnIndex;
}
private boolean isOnOpenClose(int rowIndex, int x) {
if (!rowModel.isExpandable(rowIndex)) {
return false;
}
int indent = rowModel.getIndentLevel(rowIndex) * INDENT_WIDTH;
return x >= indent && x < indent + ICON_WIDTH;
}
@Override
public void mousePressed(MouseEvent e) {
Point p = e.getPoint();
int rowIndex = p.y / rowHeight;
if (e.getButton() == 1 && isOnOpenClose(rowIndex, p.x)) {
return;
}
int index = findClosestColumnBoundary(e.getPoint().x);
if (index >= 0) {
boundaryIndex = index;
startDragX = e.getPoint().x;
originalColumnStart = columnModel.getColumn(index).getStartX();
return;
}
if (DockingUtils.isControlModifier(e) && rowIndex == selectedRow) {
clearSelectedRow();
}
else {
setSelectedRow(rowIndex);
}
}
public int findClosestColumnBoundary(int x) {
for (int i = 1; i < columnModel.getColumnCount(); i++) {
GTrableColumn<T, ?> column = columnModel.getColumn(i);
int columnStart = column.getStartX();
if (x > columnStart - TRIGGER_MARGIN && x < columnStart + TRIGGER_MARGIN) {
return i;
}
}
return -1;
}
@Override
public void mouseMoved(MouseEvent e) {
int index = findClosestColumnBoundary(e.getPoint().x);
if (index >= 0) {
setCursor(Cursor.getPredefinedCursor(Cursor.E_RESIZE_CURSOR));
}
else {
setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR));
}
}
@Override
public void mouseReleased(MouseEvent e) {
startDragX = -1;
boundaryIndex = -1;
}
@Override
public void mouseDragged(MouseEvent e) {
if (startDragX < 0) {
return;
}
int x = e.getPoint().x;
int diff = x - startDragX;
int newColumnStart = originalColumnStart + diff;
columnModel.moveColumnStart(boundaryIndex, newColumnStart);
repaint();
}
}
private class GTrableKeyListener extends KeyAdapter {
@Override
public void keyPressed(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_DOWN:
if (selectedRow < rowModel.getRowCount() - 1) {
setSelectedRow(selectedRow + 1);
scrollToSelectedRow();
e.consume();
}
break;
case KeyEvent.VK_UP:
if (selectedRow > 0) {
setSelectedRow(selectedRow - 1);
scrollToSelectedRow();
e.consume();
}
break;
case KeyEvent.VK_ENTER:
toggleOpen(selectedRow);
e.consume();
break;
}
}
@Override
public void keyReleased(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_DOWN:
if (selectedRow < rowModel.getRowCount() - 1) {
e.consume();
}
break;
case KeyEvent.VK_UP:
if (selectedRow > 0) {
e.consume();
}
break;
case KeyEvent.VK_ENTER:
e.consume();
break;
}
}
@Override
public void keyTyped(KeyEvent e) {
switch (e.getKeyCode()) {
case KeyEvent.VK_DOWN:
if (selectedRow < rowModel.getRowCount() - 1) {
e.consume();
}
break;
case KeyEvent.VK_UP:
if (selectedRow > 0) {
e.consume();
}
break;
case KeyEvent.VK_ENTER:
e.consume();
break;
}
}
}
}

View file

@ -0,0 +1,32 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking.widgets.trable;
import java.awt.event.MouseEvent;
/**
* Listener for {@link GTrable} cell clicked
*/
public interface GTrableCellClickedListener {
/**
* Notification the a GTrable cell was clicked.
* @param row the row index of the cell that was clicked
* @param column the column index of the cell that was clicked
* @param event the mouse event of the click
*/
public void cellClicked(int row, int column, MouseEvent event);
}

View file

@ -0,0 +1,40 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking.widgets.trable;
import java.awt.Component;
/**
* Interface for {@link GTrable} cell renderers
*
* @param <C> the type of the column value for this cell
*/
public interface GTrableCellRenderer<C> {
/**
* Gets and prepares the renderer component for the given column value
* @param trable the GTrable
* @param value the column value
* @param isSelected true if the row is selected
* @param hasFocus true if the cell has focus
* @param row the row of the cell being painted
* @param column the column of the cell being painted
* @return the component to use to paint the cell value
*/
public Component getCellRenderer(GTrable<?> trable, C value,
boolean isSelected, boolean hasFocus, int row, int column);
}

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.widgets.trable;
/**
* Abstract base class for {@link GTrable} column objects in the {@link GTrableColumnModel}
*
* @param <R> the row object type
* @param <C> the column value type
*/
public abstract class GTrableColumn<R, C> {
private static final int DEFAULT_MIN_SIZE = 20;
private static GTrableCellRenderer<Object> DEFAULT_RENDERER =
new DefaultGTrableCellRenderer<>();
private int startX;
private int width;
public GTrableColumn() {
width = getPreferredWidth();
}
public int getWidth() {
return width;
}
@SuppressWarnings("unchecked")
public GTrableCellRenderer<C> getRenderer() {
return (GTrableCellRenderer<C>) DEFAULT_RENDERER;
}
/**
* Returns the column value given the row object
* @param row the row object containing the data for the entire row
* @return the value to be displayed in this column
*/
public abstract C getValue(R row);
protected int getPreferredWidth() {
return 100;
}
void setWidth(int width) {
this.width = width;
}
public int getMinWidth() {
return DEFAULT_MIN_SIZE;
}
public boolean isResizable() {
return true;
}
void setStartX(int x) {
this.startX = x;
}
int getStartX() {
return startX;
}
}

View file

@ -0,0 +1,252 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking.widgets.trable;
import java.util.ArrayList;
import java.util.List;
/**
* Abstract base class for {@link GTrable} column models
*
* @param <T> the row object type
*/
public abstract class GTrableColumnModel<T> {
private List<GTrableColumn<T, ?>> columns = new ArrayList<>();
private int totalWidth;
public GTrableColumnModel() {
reloadColumns();
}
/**
* {@return the number of columns in this model.}
*/
public int getColumnCount() {
return columns.size();
}
/**
* {@return the column object for the given column index.}
* @param column the index of the column
*/
public GTrableColumn<T, ?> getColumn(int column) {
return columns.get(column);
}
/**
* {@return the preferred width of the model which is the sum of the preferred widths of each
* column.}
*/
public int getPreferredWidth() {
int preferredWidth = 0;
for (GTrableColumn<T, ?> column : columns) {
preferredWidth += column.getPreferredWidth();
}
return preferredWidth;
}
protected int computeWidth() {
int width = 0;
for (GTrableColumn<T, ?> column : columns) {
width += column.getWidth();
}
return width;
}
protected void reloadColumns() {
columns.clear();
populateColumns(columns);
computeColumnStarts();
totalWidth = computeWidth();
}
/**
* Subclasses implement this method to define the columns for this model.
* @param columnList a list to populate with column objects
*/
protected abstract void populateColumns(List<GTrableColumn<T, ?>> columnList);
protected void removeAllColumns() {
columns.removeAll(columns);
totalWidth = 0;
}
protected int getWidth() {
return totalWidth;
}
protected int getIndex(int x) {
for (int i = columns.size() - 1; i >= 0; i--) {
GTrableColumn<T, ?> column = columns.get(i);
if (x >= column.getStartX()) {
return i;
}
}
return 0;
}
protected void setWidth(int newWidth) {
int diff = newWidth - totalWidth;
if (diff == 0) {
return;
}
if (diff > 0) {
int amount = growLeftPreferred(columns.size() - 1, diff);
growLeft(columns.size() - 1, amount);
}
else {
shrinkLeft(columns.size() - 1, -diff);
}
computeColumnStarts();
}
void moveColumnStart(int columnIndex, int x) {
GTrableColumn<T, ?> column = columns.get(columnIndex);
int currentStartX = column.getStartX();
int diff = x - currentStartX;
if (diff > 0 && canGrowLeft(columnIndex - 1)) {
int actualAmount = shrinkRight(columnIndex, diff);
growLeft(columnIndex - 1, actualAmount);
}
else if (diff < 0 && canGrowRight(columnIndex)) {
int actualAmount = shrinkLeft(columnIndex - 1, -diff);
growRight(columnIndex, actualAmount);
}
computeColumnStarts();
}
private boolean canGrowLeft(int index) {
return canGrow(0, index);
}
private boolean canGrowRight(int index) {
return canGrow(index, columns.size() - 1);
}
private boolean canGrow(int index1, int index2) {
for (int i = index1; i <= index2; i++) {
if (columns.get(i).isResizable()) {
return true;
}
}
return false;
}
private void computeColumnStarts() {
int x = 0;
for (int i = 0; i < columns.size(); i++) {
GTrableColumn<T, ?> column = columns.get(i);
column.setStartX(x);
int width = column.getWidth();
x += width;
}
totalWidth = x;
modelColumnsChaged();
}
protected void modelColumnsChaged() {
// subclasses can override if they need to react to changes in the column positions or
// sizes
}
private void growRight(int columnIndex, int amount) {
for (int i = columnIndex; i < columns.size(); i++) {
GTrableColumn<T, ?> column = columns.get(i);
if (column.isResizable()) {
column.setWidth(column.getWidth() + amount);
return;
}
}
}
private void growLeft(int columnIndex, int amount) {
for (int i = columnIndex; i >= 0; i--) {
GTrableColumn<T, ?> column = columns.get(i);
if (column.isResizable()) {
column.setWidth(column.getWidth() + amount);
return;
}
}
}
private int growLeftPreferred(int columnIndex, int amount) {
for (int i = columnIndex; i >= 0 && amount > 0; i--) {
GTrableColumn<T, ?> column = columns.get(i);
if (!column.isResizable()) {
continue;
}
int width = column.getWidth();
int preferredWidth = column.getPreferredWidth();
if (width < preferredWidth) {
int adjustment = Math.min(amount, preferredWidth - width);
column.setWidth(width + adjustment);
amount -= adjustment;
}
}
return amount;
}
private int growRightPreferred(int columnIndex, int amount) {
for (int i = columnIndex; i < columns.size() && amount > 0; i++) {
GTrableColumn<T, ?> column = columns.get(i);
if (!column.isResizable()) {
continue;
}
int width = column.getWidth();
int preferredWidth = column.getPreferredWidth();
if (width < preferredWidth) {
int adjustment = Math.min(amount, preferredWidth - width);
column.setWidth(width + adjustment);
amount -= adjustment;
}
}
return amount;
}
private int shrinkLeft(int columnIndex, int amount) {
int remainingAmount = amount;
for (int i = columnIndex; i >= 0 && remainingAmount > 0; i--) {
remainingAmount -= shrinkColumn(i, remainingAmount);
}
return amount - remainingAmount;
}
private int shrinkRight(int columnIndex, int amount) {
int remainingAmount = amount;
for (int i = columnIndex; i < columns.size() && remainingAmount > 0; i++) {
remainingAmount -= shrinkColumn(i, remainingAmount);
}
return amount - remainingAmount;
}
private int shrinkColumn(int columnIndex, int amount) {
GTrableColumn<T, ?> column = columns.get(columnIndex);
if (!column.isResizable()) {
return 0;
}
int currentWidth = column.getWidth();
int minWidth = column.getMinWidth();
if (currentWidth >= minWidth + amount) {
column.setWidth(currentWidth - amount);
return amount;
}
column.setWidth(minWidth);
return currentWidth - minWidth;
}
}

View file

@ -0,0 +1,27 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking.widgets.trable;
/**
* The listener interface for when the row model changes.
*/
public interface GTrableModeRowlListener {
/**
* Notification that the row model changed
*/
public void trableChanged();
}

View file

@ -0,0 +1,66 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking.widgets.trable;
import java.util.List;
/**
* Abstract base class for {@link GTrable} row objects.
*
* @param <T> the row object type
*/
public abstract class GTrableRow<T extends GTrableRow<T>> {
private final int indentLevel;
private boolean isExpanded = false;
/**
* Constructor
* @param indentLevel the indent level of this row
*/
protected GTrableRow(int indentLevel) {
this.indentLevel = indentLevel;
}
/**
* {@return the indent level for this row}
*/
public int getIndentLevel() {
return indentLevel;
}
/**
* {@return true if this row is expandable}
*/
public abstract boolean isExpandable();
/**
* {@return true if this node is expanded.}
*/
public boolean isExpanded() {
return isExpanded;
}
/**
* Sets the expanded state.
* @param expanded true if this row is expanded
*/
void setExpanded(boolean expanded) {
this.isExpanded = expanded;
}
protected abstract List<T> getChildRows();
}

View file

@ -0,0 +1,84 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking.widgets.trable;
/**
* Row model for a {@link GTrable}.
*
* @param <T> the row object type
*/
public interface GTrableRowModel<T> {
/**
* {@return the total number of rows include open child rows.}
*/
public int getRowCount();
/**
* {@return the row object for the given index.}
* @param rowIndex the index of the row to retrieve
*/
public T getRow(int rowIndex);
/**
* {@return true if the row at the given index can be expanded}
* @param rowIndex the row to test if expandable
*/
public boolean isExpandable(int rowIndex);
/**
* {@return true if the row at the given index is expanded.}
* @param rowIndex the index of the row to test for expanded
*/
public boolean isExpanded(int rowIndex);
/**
* Collapse the row at the given row index.
* @param rowIndex the index of the row to collapse
* @return the total number of rows removed due to collapsing the row
*/
public int collapseRow(int rowIndex);
/**
* Expand the row at the given row index.
* @param rowIndex the index of the row to expand
* @return the total number of rows added due to the expand
*/
public int expandRow(int rowIndex);
/**
* {@return the indent level of the row at the given index.}
* @param rowIndex the index of the row to get its indent level
*/
public int getIndentLevel(int rowIndex);
/**
* Adds a listener to the list that is notified each time a change
* to the data model occurs.
*
* @param l the listener to be notified
*/
public void addListener(GTrableModeRowlListener l);
/**
* Removes a listener from the list that is notified each time a
* change to the model occurs.
*
* @param l the listener to remove
*/
public void removeListener(GTrableModeRowlListener l);
}

View file

@ -0,0 +1,105 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking.widgets.trable;
import java.awt.*;
import javax.swing.Icon;
import generic.theme.GThemeDefaults.Colors;
/**
* Icon used for the expand/collapse control in a {@link GTrable}
*/
public class OpenCloseIcon implements Icon {
private int width;
private int height;
private int[] xPoints;
private int[] yPoints;
private Color color = Colors.FOREGROUND;
/**
* Constructor
* @param isOpen if true, draws an icon that indicates the row is open, otherwise draws an
* icon that the icon indicates the row is closed
* @param width the width to draw the icon
* @param height the height to draw the icon
*/
public OpenCloseIcon(boolean isOpen, int width, int height) {
this.width = width;
this.height = height;
if (isOpen) {
buildDownPointingTriangle();
}
else {
buildRightPointingTriangle();
}
}
public void setColor(Color color) {
this.color = color;
}
private void buildDownPointingTriangle() {
int triangleWidth = 8;
int triangleHeight = 4;
int startX = width / 2 - triangleWidth / 2;
int endX = startX + triangleWidth;
int startY = height / 2 - triangleHeight / 2;
int endY = startY + triangleHeight;
xPoints = new int[] { startX, endX, (startX + endX) / 2 };
yPoints = new int[] { startY, startY, endY };
}
private void buildRightPointingTriangle() {
int triangleWidth = 4;
int triangleHeight = 8;
int startX = width / 2 - triangleWidth / 2;
int endX = startX + triangleWidth;
int startY = height / 2 - triangleHeight / 2;
int endY = startY + triangleHeight;
xPoints = new int[] { startX, endX, startX };
yPoints = new int[] { startY, (startY + endY) / 2, endY };
}
@Override
public void paintIcon(Component c, Graphics g, int x, int y) {
g.setColor(color);
g.translate(x, y);
Graphics2D graphics2D = (Graphics2D) g;
graphics2D.drawPolygon(xPoints, yPoints, 3);
graphics2D.fillPolygon(xPoints, yPoints, 3);
g.translate(-x, -y);
}
@Override
public int getIconWidth() {
return width;
}
@Override
public int getIconHeight() {
return height;
}
}

View file

@ -0,0 +1,338 @@
/* ###
* IP: GHIDRA
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package docking.widgets.trable;
import static org.junit.Assert.*;
import java.util.*;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import org.apache.commons.lang3.StringUtils;
import org.junit.Before;
import org.junit.Test;
import generic.test.AbstractGuiTest;
import ghidra.util.datastruct.Range;
public class GTrableTest extends AbstractGuiTest {
private GTrableRowModel<TestDataRow> rowModel;
private TestColumnModel columnModel;
private GTrable<TestDataRow> gTrable;
private JFrame frame;
@Before
public void setUp() {
rowModel = createRowModel();
columnModel = new TestColumnModel();
gTrable = new GTrable<TestDataRow>(rowModel, columnModel);
gTrable.setPreferredVisibleRowCount(3, 3);
JScrollPane scroll = new JScrollPane(gTrable);
frame = new JFrame("Test");
frame.getContentPane().add(scroll);
frame.pack();
frame.setVisible(true);
}
@Test
public void testInitialState() {
//@formatter:off
assertAllRows(
"a",
"b",
"c"
);
assertVisibleRows(
"a",
"b",
"c"
);
//@formatter:on
Range visibleRows = gTrable.getVisibleRows();
assertEquals(0, visibleRows.min);
assertEquals(2, visibleRows.max);
assertTrue(rowModel.getRow(0).isExpandable());
assertFalse(rowModel.getRow(1).isExpandable());
assertTrue(rowModel.getRow(2).isExpandable());
}
@Test
public void testExpandRow() {
selectRow(1);
//@formatter:off
assertVisibleRows(
"a",
"b",
"c"
);
//@formatter:on
assertTrue(rowModel.getRow(0).isExpandable());
expandRow(0);
//@formatter:off
assertVisibleRows(
"a",
" a.1",
" a.2"
);
assertAllRows(
"a",
" a.1",
" a.2",
" a.3",
"b",
"c"
);
//@formatter:on
assertEquals(4, gTrable.getSelectedRow());
}
@Test
public void testCollapseRow() {
expandRow(0);
selectRow(5);
//@formatter:off
assertVisibleRows(
"a",
" a.1",
" a.2"
);
assertAllRows(
"a",
" a.1",
" a.2",
" a.3",
"b",
"c"
);
//@formatter:on
assertTrue(rowModel.isExpanded(0));
collapseRow(0);
//@formatter:off
assertVisibleRows(
"a",
"b",
"c"
);
assertAllRows(
"a",
"b",
"c"
);
//@formatter:on
assertEquals(2, gTrable.getSelectedRow());
}
@Test
public void testExpandAllRow() {
//@formatter:off
assertVisibleRows(
"a",
"b",
"c"
);
//@formatter:on
expandAll();
//@formatter:off
assertVisibleRows(
"a",
" a.1",
" a.2"
);
assertAllRows(
"a",
" a.1",
" a.2",
" a.2.A",
" a.2.B",
" a.2.C",
" a.3",
"b",
"c",
" c.1",
" c.2"
);
//@formatter:on
}
@Test
public void testScrollToSelectedRow() {
expandAll();
selectRow(5);
//@formatter:off
assertVisibleRows(
"a",
" a.1",
" a.2"
);
//@formatter:on
scrollToSelectedRow();
//@formatter:off
assertVisibleRows(
" a.2.A",
" a.2.B",
" a.2.C"
);
//@formatter:on
}
private void scrollToSelectedRow() {
runSwing(() -> {
gTrable.scrollToSelectedRow();
});
waitForSwing();
}
private void expandRow(int row) {
runSwing(() -> {
gTrable.expandRow(row);
});
waitForSwing();
}
private void collapseRow(int row) {
runSwing(() -> {
gTrable.collapseRow(row);
});
waitForSwing();
}
private void expandAll() {
runSwing(() -> {
gTrable.expandAll();
});
waitForSwing();
}
private void selectRow(int row) {
runSwing(() -> {
gTrable.setSelectedRow(row);
});
}
private void assertAllRows(String... expectedRows) {
List<String> actualRows = getRowsAsText(0, rowModel.getRowCount() - 1);
assertEquals(expectedRows.length, actualRows.size());
List<String> expectedList = Arrays.asList(expectedRows);
assertListEqualOrdered(expectedList, actualRows);
}
private void assertVisibleRows(String... expectedRows) {
Range visibleRows = gTrable.getVisibleRows();
List<String> actualRows = getRowsAsText(visibleRows.min, visibleRows.max);
assertEquals(expectedRows.length, actualRows.size());
List<String> expectedList = Arrays.asList(expectedRows);
assertListEqualOrdered(expectedList, actualRows);
}
private List<String> getRowsAsText(int startRow, int endRow) {
List<String> list = new ArrayList<>();
for (int i = startRow; i <= endRow; i++) {
TestDataRow row = rowModel.getRow(i);
int indent = row.getIndentLevel();
String name = row.getName();
String indentation = StringUtils.repeat("\t", indent);
list.add(indentation + name);
}
return list;
}
private GTrableRowModel<TestDataRow> createRowModel() {
TestDataRow a2A = new TestDataRow("a.2.A", 2, null);
TestDataRow a2B = new TestDataRow("a.2.B", 2, null);
TestDataRow a2C = new TestDataRow("a.2.C", 2, null);
TestDataRow a1 = new TestDataRow("a.1", 1, null);
TestDataRow a2 = new TestDataRow("a.2", 1, List.of(a2A, a2B, a2C));
TestDataRow a3 = new TestDataRow("a.3", 1, null);
TestDataRow c1 = new TestDataRow("c.1", 1, null);
TestDataRow c2 = new TestDataRow("c.2", 1, null);
TestDataRow a = new TestDataRow("a", 0, List.of(a1, a2, a3));
TestDataRow b = new TestDataRow("b", 0, null);
TestDataRow c = new TestDataRow("c", 0, List.of(c1, c2));
return new DefaultGTrableRowModel<>(List.of(a, b, c));
}
class TestDataRow extends GTrableRow<TestDataRow> {
private List<TestDataRow> children;
private String name;
protected TestDataRow(String name, int indentLevel, List<TestDataRow> children) {
super(indentLevel);
this.name = name;
this.children = children;
}
public String getName() {
return name;
}
@Override
public boolean isExpandable() {
return children != null;
}
@Override
protected List<TestDataRow> getChildRows() {
return children;
}
}
private class NameColumn extends GTrableColumn<TestDataRow, String> {
@Override
public String getValue(TestDataRow row) {
return row.getName();
}
@Override
protected int getPreferredWidth() {
return 150;
}
}
class TestColumnModel extends GTrableColumnModel<TestDataRow> {
@Override
protected void populateColumns(List<GTrableColumn<TestDataRow, ?>> columnList) {
columnList.add(new NameColumn());
}
}
}