From f9af85a31d80e092697b715ab9f6e970cc4fc525 Mon Sep 17 00:00:00 2001 From: ghidragon <106987263+ghidragon@users.noreply.github.com> Date: Mon, 11 Dec 2023 17:06:01 -0500 Subject: [PATCH] GP-2147 added ability to resize views in the byteviewer by dragging inthe header bar. --- .../ByteViewerPlugin/The_Byte_Viewer.htm | 5 +- .../ByteViewerComponentProvider.java | 40 +- .../core/byteviewer/ByteViewerHeader.java | 204 ---------- .../byteviewer/ByteViewerIndexedView.java | 226 +++++++++++ .../core/byteviewer/ByteViewerPanel.java | 372 +++--------------- .../plugin/core/byteviewer/DataModelInfo.java | 53 --- .../core/byteviewer/IndexFieldFactory.java | 58 +-- .../byteviewer/InteractivePanelManager.java | 304 ++++++++++++++ .../byteviewer/ByteViewerPlugin1Test.java | 109 +++-- .../indexedscrollpane/IndexedScrollable.java | 1 + 10 files changed, 714 insertions(+), 658 deletions(-) delete mode 100644 Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerHeader.java create mode 100644 Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerIndexedView.java delete mode 100644 Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/DataModelInfo.java create mode 100644 Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/InteractivePanelManager.java diff --git a/Ghidra/Features/ByteViewer/src/main/help/help/topics/ByteViewerPlugin/The_Byte_Viewer.htm b/Ghidra/Features/ByteViewer/src/main/help/help/topics/ByteViewerPlugin/The_Byte_Viewer.htm index 5810dc25d8..e4502d64f5 100644 --- a/Ghidra/Features/ByteViewer/src/main/help/help/topics/ByteViewerPlugin/The_Byte_Viewer.htm +++ b/Ghidra/Features/ByteViewer/src/main/help/help/topics/ByteViewerPlugin/The_Byte_Viewer.htm @@ -277,11 +277,14 @@ -

Reorder Views

+

Reorder / Resize Views

The various views in the ByteViewer can be reordered by dragging the view header to the left or right of its current position. The view positions are swapped.

+

The width of each view can also be changed by dragging the separator + bars in the view header to the left or right. This will resize the view that is to + the left of the separator bar.

Writing Your Own Format Plugin

diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerComponentProvider.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerComponentProvider.java index 9908d4fafb..33175e0944 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerComponentProvider.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerComponentProvider.java @@ -48,6 +48,7 @@ public abstract class ByteViewerComponentProvider extends ComponentProviderAdapt protected static final String X_OFFSET = "X Offset"; protected static final String Y_OFFSET = "Y Offset"; private static final String VIEW_NAMES = "View Names"; + private static final String VIEW_WIDTHS = "View_Widths"; private static final String HEX_VIEW_GROUPSIZE = "Hex view groupsize"; private static final String BYTES_PER_LINE_NAME = "Bytes Per Line"; private static final String OFFSET_NAME = "Offset"; @@ -69,7 +70,7 @@ public abstract class ByteViewerComponentProvider extends ComponentProviderAdapt static final GColor CURRENT_LINE_COLOR = GhidraOptions.DEFAULT_CURSOR_LINE_COLOR; //@formatter:on - static final String DEFAULT_INDEX_NAME = "Addresses"; + static final String INDEX_COLUMN_NAME = "Addresses"; static final String SEPARATOR_COLOR_OPTION_NAME = "Block Separator Color"; static final String CHANGED_VALUE_COLOR_OPTION_NAME = "Changed Values Color"; @@ -209,19 +210,17 @@ public abstract class ByteViewerComponentProvider extends ComponentProviderAdapt help, "Color of cursor in the active view."); opt.registerThemeColorBinding(CURSOR_NON_ACTIVE_COLOR_OPTION_NAME, - CURSOR_NON_ACTIVE_COLOR.getId(), - help, "Color of cursor in the non-active views."); + CURSOR_NON_ACTIVE_COLOR.getId(), help, "Color of cursor in the non-active views."); opt.registerThemeColorBinding(CURSOR_NOT_FOCUSED_COLOR_OPTION_NAME, - CURSOR_NOT_FOCUSED_COLOR.getId(), - help, "Color of cursor when the byteview does not have focus."); + CURSOR_NOT_FOCUSED_COLOR.getId(), help, + "Color of cursor when the byteview does not have focus."); opt.registerThemeColorBinding(CURRENT_LINE_COLOR_OPTION_NAME, GhidraOptions.DEFAULT_CURSOR_LINE_COLOR.getId(), help, "Color of the line containing the cursor"); - opt.registerThemeFontBinding(OPTION_FONT, DEFAULT_FONT_ID, help, - "Font used in the views."); + opt.registerThemeFontBinding(OPTION_FONT, DEFAULT_FONT_ID, help, "Font used in the views."); opt.registerOption(OPTION_HIGHLIGHT_CURSOR_LINE, true, help, "Toggles highlighting background color of line containing the cursor"); @@ -332,11 +331,19 @@ public abstract class ByteViewerComponentProvider extends ComponentProviderAdapt } protected void writeConfigState(SaveState saveState) { - DataModelInfo info = panel.getDataModelInfo(); - saveState.putStrings(VIEW_NAMES, info.getNames()); + List viewNames = panel.getViewNamesInDisplayOrder(); + saveState.putStrings(VIEW_NAMES, viewNames.toArray(new String[viewNames.size()])); saveState.putInt(HEX_VIEW_GROUPSIZE, hexGroupSize); saveState.putInt(BYTES_PER_LINE_NAME, bytesPerLine); saveState.putInt(OFFSET_NAME, offset); + SaveState columnState = new SaveState(VIEW_WIDTHS); + int indexWidth = panel.getViewWidth(INDEX_COLUMN_NAME); + columnState.putInt(INDEX_COLUMN_NAME, indexWidth); + for (String viewName : viewNames) { + int width = panel.getViewWidth(viewName); + columnState.putInt(viewName, width); + } + saveState.putSaveState(VIEW_WIDTHS, columnState); } protected void readConfigState(SaveState saveState) { @@ -346,6 +353,17 @@ public abstract class ByteViewerComponentProvider extends ComponentProviderAdapt bytesPerLine = saveState.getInt(BYTES_PER_LINE_NAME, DEFAULT_BYTES_PER_LINE); offset = saveState.getInt(OFFSET_NAME, 0); panel.restoreConfigState(bytesPerLine, offset); + SaveState viewWidths = saveState.getSaveState(VIEW_WIDTHS); + if (viewWidths != null) { + String[] viewNames = viewWidths.getNames(); + for (String viewName : viewNames) { + int width = viewWidths.getInt(viewName, 0); + if (width > 0) { + panel.setViewWidth(viewName, width); + } + + } + } } /** @@ -422,9 +440,7 @@ public abstract class ByteViewerComponentProvider extends ComponentProviderAdapt } public Set getCurrentViews() { - DataModelInfo info = panel.getDataModelInfo(); - HashSet currentViewNames = new HashSet<>(Arrays.asList(info.getNames())); - return currentViewNames; + return new HashSet(panel.getViewNamesInDisplayOrder()); } private void refreshView() { diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerHeader.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerHeader.java deleted file mode 100644 index a3b15b5438..0000000000 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerHeader.java +++ /dev/null @@ -1,204 +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.byteviewer; - -import java.awt.*; -import java.util.HashMap; -import java.util.Iterator; - -import javax.swing.*; -import javax.swing.event.TableColumnModelListener; -import javax.swing.table.*; - -import generic.theme.Gui; -import ghidra.util.table.GhidraTable; - -/** - * JTableHeader that uses the default table column model to manage - * TableColumns. Sizes the column according to its corresponding viewer - * component. Allows columns to be moved. - */ -class ByteViewerHeader extends JTableHeader implements Scrollable { - - private static final String FONT_ID = "font.byteviewer.header"; - private TableColumnModel columnModel; - private Component container; - - private int separatorWidth; - private HashMap components; // table of components that map to columns - - /** - * Constructor - * @param container Container that will be used to calculate the - * preferred size - */ - ByteViewerHeader(Component container) { - - super(); - - this.container = container; - components = new HashMap(); - Gui.registerFont(this, FONT_ID); - setResizingAllowed(false); - table = new GhidraTable(); - setTable(table); - table.setTableHeader(this); - columnModel = getColumnModel(); - columnModel.setColumnMargin(0); - JSeparator s = new JSeparator(SwingConstants.VERTICAL); - separatorWidth = s.getPreferredSize().width; - } - - /** - * Add a new column. - * @param name name that will be displayed in the column - * @param c corresponding viewer component - */ - public void addColumn(String name, Component c) { - - TableColumn col = new TableColumn(components.size()); - - col.setHeaderValue(name); -// col.setMinWidth((2*margin) + name.length() * unitWidth); - col.setIdentifier(c); - columnModel.addColumn(col); - components.put(c, col); - resizeAndRepaint(); - - } - - /** - * Remove a column. - * @param c component that corresponds to a column to be removed. - */ - public void removeColumn(Component c) { - TableColumn col = components.get(c); - - if (col != null) { - columnModel.removeColumn(col); - components.remove(c); - setSize(getPreferredSize()); - resizeAndRepaint(); - } - } - - /** - * Get the preferred size of the table header. - */ - @Override - public Dimension getPreferredSize() { - Dimension d = super.getPreferredSize(); - d.height += 4; - return d; - } - - /** - * Set the name on the column. - * @param c component that maps to the column - * @param name name to set on the column header - */ - public void setColumnName(Component c, String name) { - TableColumn col = components.get(c); - - if (col != null) { - col.setHeaderValue(name); - recomputeColumnHeaders(); - resizeAndRepaint(); - } - } - - /** - * Add a column model listener. - */ - public void addColumnModelListener(TableColumnModelListener l) { - columnModel.addColumnModelListener(l); - } - - /** - * Remove a column model listener. - */ - public void removeColumnModelListener(TableColumnModelListener l) { - columnModel.removeColumnModelListener(l); - } - - @Override - public void paint(Graphics g) { - recomputeColumnHeaders(); - super.paint(g); - } - - // Scrollable interface methods - - @Override - public Dimension getPreferredScrollableViewportSize() { - return getPreferredSize(); - } - - @Override - public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { - return 1; - } - - @Override - public boolean getScrollableTracksViewportHeight() { - return false; - } - - @Override - public boolean getScrollableTracksViewportWidth() { - return false; - } - - @Override - public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { - return 1; - } - - ///////////////////////////////////////////////////////////////// - // *** private methods *** - ///////////////////////////////////////////////////////////////// - /** - * Recompute the width of the column headers based on the width - * of the corresponding component. - */ - private void recomputeColumnHeaders() { - - Iterator iter = components.keySet().iterator(); - - while (iter.hasNext()) { - - Component c = iter.next(); - TableColumn col = components.get(c); - int width = c.getPreferredSize().width; - int index = columnModel.getColumnIndex(col.getIdentifier()); - - if (index == 0) { - width += separatorWidth / 2; - } - else if (index == components.size() - 1) { - width += separatorWidth / 2; - } - else { - width += separatorWidth; - } - col.setMinWidth(width); - col.setMaxWidth(width); - col.setPreferredWidth(width); - } - - } - -} diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerIndexedView.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerIndexedView.java new file mode 100644 index 0000000000..e6617e38d9 --- /dev/null +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerIndexedView.java @@ -0,0 +1,226 @@ +/* ### + * 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.byteviewer; + +import java.awt.BorderLayout; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.*; + +import docking.widgets.fieldpanel.FieldPanel; +import docking.widgets.fieldpanel.Layout; +import docking.widgets.indexedscrollpane.*; +import generic.theme.GColor; +import generic.theme.Gui; +import ghidra.app.plugin.core.format.DataFormatModel; + +/** + * Main ByteViewer component that is scrolled in a {@link IndexedScrollPane}. Similar to the view + * component in a {@link JScrollPane}. This component manages two or more {@link FieldPanel} + * components. It always contains the "index" field panel which displays the addresses of the + * values being displayed. Then it has one or more data FieldPanels; one for each format being + * displayed (e.g., hex, octal, binary). + *

+ * There is also header component that displays the name of each column and allows the user to + * reorder and resize the views. This class uses an {@link InteractivePanelManager} to handle + * the reordering and resizing of the views in coordination with the header component. It is the + * client's responsibility to get the header component and install it into the IndexedScrollPane. + */ +class ByteViewerIndexedView extends JPanel implements IndexedScrollable, IndexScrollListener { + private static final String HEADER_FONT_ID = "font.byteviewer.header"; + private FieldPanel indexPanel; + private List allPanels = new ArrayList<>(); + private boolean processingIndexRangeChanged; + private InteractivePanelManager panelManager; + + ByteViewerIndexedView(FieldPanel indexPanel) { + super(new BorderLayout()); + this.indexPanel = indexPanel; + allPanels.add(indexPanel); + panelManager = new InteractivePanelManager(); + panelManager.setHeaderFont(Gui.getFont(HEADER_FONT_ID)); + + indexPanel.addIndexScrollListener(this); + + panelManager.addComponent(ByteViewerComponentProvider.INDEX_COLUMN_NAME, indexPanel); + JComponent mainPanel = panelManager.getMainPanel(); + add(mainPanel, BorderLayout.CENTER); + mainPanel.setBackground(new GColor("color.bg.byteviewer")); + + addMouseWheelListener(e -> { + // this lets us scroll the byte viewer when the user is not over any panel, but still + // over the view + Layout firstLayout = indexPanel.getLayoutModel().getLayout(BigInteger.ZERO); + int layoutScrollHt = firstLayout != null // + ? firstLayout.getScrollableUnitIncrement(0, 1) + : 0; + + double wheelRotation = e.getPreciseWheelRotation(); + int scrollAmount = + (int) (wheelRotation * layoutScrollHt * FieldPanel.MOUSEWHEEL_LINES_TO_SCROLL); + + indexPanel.scrollView(scrollAmount); + e.consume(); + }); + } + + void addView(String viewName, ByteViewerComponent c) { + panelManager.addComponent(viewName, c); + allPanels.add(c); + c.addIndexScrollListener(this); + } + + void removeView(ByteViewerComponent c) { + panelManager.removeComponent(c); + allPanels.remove(c); + c.removeIndexScrollListener(this); + } + + JComponent getColumnHeader() { + return panelManager.getColumnHeader(); + } + + public List getViewNamesInDisplayOrder() { + List viewNames = new ArrayList<>(); + List components = panelManager.getComponents(); + for (JComponent component : components) { + if (component == indexPanel) { + continue; + } + if (component instanceof ByteViewerComponent byteViewerComponent) { + DataFormatModel model = byteViewerComponent.getDataModel(); + viewNames.add(model.getName()); + } + } + return viewNames; + } + + void resetViewWidthToDefaults() { + List viewNames = getViewNamesInDisplayOrder(); + for (String viewName : viewNames) { + panelManager.resetColumnWidthToPreferredWidth(viewName); + } + } + + void setIndexName(String indexName) { + panelManager.setName(indexPanel, indexName); + } + + int getViewWidth(String viewName) { + return panelManager.getColumnWidth(viewName); + } + + void setColumnWidth(String viewName, int width) { + panelManager.setColumnWidth(viewName, width); + } + + @Override + public void indexRangeChanged(BigInteger startIndex, BigInteger endIndex, int yStart, + int yEnd) { + if (processingIndexRangeChanged) { + return; + } + processingIndexRangeChanged = true; + try { + // need to sync up the view position of all views when any view is scrolled + for (FieldPanel fieldPanel : allPanels) { + fieldPanel.showIndex(startIndex, yStart); + } + } + finally { + processingIndexRangeChanged = false; + } + + } + + @Override + public void addIndexScrollListener(IndexScrollListener listener) { + indexPanel.addIndexScrollListener(listener); + } + + @Override + public int getHeight(BigInteger index) { + return indexPanel.getHeight(index); + } + + @Override + public BigInteger getIndexAfter(BigInteger index) { + return indexPanel.getIndexAfter(index); + } + + @Override + public BigInteger getIndexBefore(BigInteger index) { + return indexPanel.getIndexBefore(index); + } + + @Override + public BigInteger getIndexCount() { + return indexPanel.getIndexCount(); + } + + @Override + public boolean isUniformIndex() { + return true; + } + + @Override + public void removeIndexScrollListener(IndexScrollListener listener) { + indexPanel.removeIndexScrollListener(listener); + } + + @Override + public void scrollLineDown() { + indexPanel.scrollLineDown(); + } + + @Override + public void scrollLineUp() { + indexPanel.scrollLineUp(); + } + + @Override + public void scrollPageDown() { + indexPanel.scrollPageDown(); + } + + @Override + public void scrollPageUp() { + indexPanel.scrollPageUp(); + } + + @Override + public void showIndex(BigInteger index, int verticalOffset) { + indexPanel.showIndex(index, verticalOffset); + } + + @Override + public void indexModelChanged() { + // handled by indexPanel + } + + @Override + public void indexModelDataChanged(BigInteger start, BigInteger end) { + // handled by indexPanel + } + + @Override + public void mouseWheelMoved(double preciseWheelRotation, boolean isHorizontal) { + indexPanel.mouseWheelMoved(preciseWheelRotation, isHorizontal); + } + +} diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerPanel.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerPanel.java index e968ce2fd6..0d14d964e4 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerPanel.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/ByteViewerPanel.java @@ -21,17 +21,15 @@ import java.util.ArrayList; import java.util.List; import javax.swing.*; -import javax.swing.event.*; import docking.widgets.fieldpanel.*; import docking.widgets.fieldpanel.field.EmptyTextField; import docking.widgets.fieldpanel.field.Field; import docking.widgets.fieldpanel.listener.*; import docking.widgets.fieldpanel.support.*; -import docking.widgets.indexedscrollpane.*; +import docking.widgets.indexedscrollpane.IndexedScrollPane; import docking.widgets.label.GDLabel; import docking.widgets.label.GLabel; -import generic.theme.GColor; import generic.theme.Gui; import ghidra.app.plugin.core.format.*; import ghidra.app.util.viewer.listingpanel.AddressSetDisplayListener; @@ -40,7 +38,6 @@ import ghidra.program.model.address.AddressSetView; import ghidra.util.HelpLocation; import ghidra.util.Msg; import ghidra.util.exception.InvalidInputException; -import ghidra.util.layout.HorizontalLayout; import ghidra.util.layout.PairLayout; import help.Help; import help.HelpService; @@ -49,10 +46,8 @@ import help.HelpService; * Top level component that has a scrolled pane for the panel of components that show the * view for each format. */ -public class ByteViewerPanel extends JPanel - implements TableColumnModelListener, LayoutModel, LayoutListener { +public class ByteViewerPanel extends JPanel implements LayoutModel, LayoutListener { private static final String FONT_STATUS_ID = "font.byteviewer.status"; -// private ByteViewerPlugin plugin; private List viewList; // list of field viewers private FieldPanel indexPanel; // panel for showing indexes private IndexFieldFactory indexFactory; @@ -61,12 +56,9 @@ public class ByteViewerPanel extends JPanel private JLabel offsetField; private JLabel insertionField; private JPanel statusPanel; - private CompositePanel compPanel; private int fontHeight; - private FontMetrics fm; + private FontMetrics fontMetrics; private int bytesPerLine; - private IndexedScrollPane scrollp; - private ByteViewerHeader columnHeader; private ByteBlockSet blockSet; private ByteBlock[] blocks; private IndexMap indexMap; // maps indexes to the correct block and offset @@ -78,12 +70,12 @@ public class ByteViewerPanel extends JPanel private Color highlightColor; private int highlightButton; private List layoutListeners = new ArrayList<>(1); - private int indexPanelWidth; private boolean addingView; // don't respond to cursor location // changes while this flag is true private final ByteViewerComponentProvider provider; private List displayListeners = new ArrayList<>(); + private ByteViewerIndexedView indexedView; protected ByteViewerPanel(ByteViewerComponentProvider provider) { super(); @@ -136,45 +128,6 @@ public class ByteViewerPanel extends JPanel super.paintComponent(g); } - // TableColumnModelListener interface methods - @Override - public void columnAdded(TableColumnModelEvent e) { - // no-op - } - - @Override - public void columnMarginChanged(ChangeEvent e) { - // no-op - } - - /** - * Interface method called when the columns move. - */ - @Override - public void columnMoved(TableColumnModelEvent e) { - - int fromIndex = e.getFromIndex(); - int toIndex = e.getToIndex(); - if (fromIndex == toIndex) { - return; - } - compPanel.swapView(fromIndex, toIndex); - - invalidate(); - validate(); - repaint(); - } - - @Override - public void columnRemoved(TableColumnModelEvent e) { - // no-op - } - - @Override - public void columnSelectionChanged(ListSelectionEvent e) { - // no-op - } - ////////////////////////////////////////////////////////////////////////// // ** package-level methods ** ////////////////////////////////////////////////////////////////////////// @@ -252,17 +205,14 @@ public class ByteViewerPanel extends JPanel endField.setText(lastBlock .getLocationRepresentation(lastBlock.getLength().subtract(BigInteger.ONE))); - indexPanelWidth = getIndexPanelWidth(blocks); - int center = indexPanelWidth / 2; - int startx = center - getMaxIndexSize() / 2; - indexFactory.setStartX(startx); clearSelection(); } } if (indexMap == null) { indexMap = new IndexMap(); } - indexFactory.setIndexMap(indexMap, indexPanelWidth); + indexFactory.setIndexMap(indexMap); + indexFactory.setSize(getIndexSizeInChars()); // Do the following loop twice - once with update off and then with update on. // need to do this because all the byte view components must have their models @@ -278,7 +228,7 @@ public class ByteViewerPanel extends JPanel c.setIndexMap(indexMap); } if (blocks != null && blocks.length > 0) { - columnHeader.setColumnName(indexPanel, blocks[0].getIndexName()); + indexedView.setIndexName(blocks[0].getIndexName()); } indexPanel.dataChanged(BigInteger.ZERO, indexMap.getNumIndexes()); indexSetChanged(); @@ -366,7 +316,8 @@ public class ByteViewerPanel extends JPanel } protected ByteViewerComponent newByteViewerComponent(DataFormatModel model) { - return new ByteViewerComponent(this, new ByteViewerLayoutModel(), model, bytesPerLine, fm); + return new ByteViewerComponent(this, new ByteViewerLayoutModel(), model, bytesPerLine, + fontMetrics); } /** @@ -399,10 +350,7 @@ public class ByteViewerPanel extends JPanel c.setHighlightButton(highlightButton); viewList.add(c); c.setSize(c.getPreferredSize()); - compPanel.addByteViewerComponent(c); - // tell column header it needs to grow - columnHeader.addColumn(viewName, c); - + indexedView.addView(viewName, c); c.addListeners(); if (viewList.size() == 1) { @@ -441,8 +389,7 @@ public class ByteViewerPanel extends JPanel void removeView(ByteViewerComponent comp) { viewList.remove(comp); - compPanel.removeByteViewerComponent(comp); - columnHeader.removeColumn(comp); + indexedView.removeView(comp); if (currentView == comp) { currentView = null; @@ -485,10 +432,6 @@ public class ByteViewerPanel extends JPanel ByteViewerComponent c = viewList.get(i); c.refreshView(); } -// PluginEvent lastSelectionEvent = plugin.getLastSelectionEvent(); -// if (lastSelectionEvent != null) { -// plugin.firePluginEvent(lastSelectionEvent); -// } } int getNumberOfViews() { @@ -509,9 +452,11 @@ public class ByteViewerPanel extends JPanel this.bytesPerLine = bytesPerLine; updateIndexMap(); } + // reset view column widths to preferred width for new bytesPerline + indexedView.resetViewWidthToDefaults(); + // force everything to get validated, or else the // header columns do not get repainted properly... - invalidate(); validate(); repaint(); @@ -615,36 +560,11 @@ public class ByteViewerPanel extends JPanel } FontMetrics getCurrentFontMetrics() { - return fm; + return fontMetrics; } - /** - * Return array of names of views in the order that they appear in the panel. The name array - * includes an entry for the index panel. - * @return a DataModelInfo object that describes the current views - */ - DataModelInfo getDataModelInfo() { - - DataModelInfo info = new DataModelInfo(viewList.size()); - Component[] c = compPanel.getComponents(); - int index = 0; - for (Component element : c) { - if (element instanceof JSeparator) { - continue; - } - if (element == indexPanel) { - // don't put the index panel into the data model info, as it is not configurable - continue; - } - else if (element instanceof ByteViewerComponent) { - DataFormatModel model = ((ByteViewerComponent) element).getDataModel(); - String name = model.getName(); - int groupSize = model.getGroupSize(); - info.set(index, name, groupSize); - ++index; - } - } - return info; + List getViewNamesInDisplayOrder() { + return indexedView.getViewNamesInDisplayOrder(); } /** @@ -679,11 +599,11 @@ public class ByteViewerPanel extends JPanel /** * Restore the configuration of the plugin. * - * @param fontMetrics font metrics + * @param metrics font metrics * @param newEditColor color for showing edits */ - void restoreConfigState(FontMetrics fontMetrics, Color newEditColor) { - setFontMetrics(fontMetrics); + void restoreConfigState(FontMetrics metrics, Color newEditColor) { + setFontMetrics(metrics); setEditColor(newEditColor); } @@ -704,21 +624,13 @@ public class ByteViewerPanel extends JPanel } void setFontMetrics(FontMetrics fm) { - this.fm = fm; + this.fontMetrics = fm; for (int i = 0; i < viewList.size(); i++) { ByteViewerComponent c = viewList.get(i); c.setFontMetrics(fm); } indexFactory = new IndexFieldFactory(fm); - - int charWidth = fm.charWidth('W'); - indexFactory.setStartX(charWidth); - indexPanelWidth = - ByteViewerComponentProvider.DEFAULT_NUMBER_OF_CHARS * charWidth + (2 * charWidth); - if (blocks != null) { - indexPanelWidth = getIndexPanelWidth(blocks); - } - indexFactory.setIndexMap(indexMap, indexPanelWidth); + indexFactory.setSize(getIndexSizeInChars()); indexPanel.modelSizeChanged(IndexMapper.IDENTITY_MAPPER); } @@ -731,7 +643,7 @@ public class ByteViewerPanel extends JPanel } protected FontMetrics getFontMetrics() { - return fm; + return fontMetrics; } protected int getBytesPerLine() { @@ -745,13 +657,11 @@ public class ByteViewerPanel extends JPanel setLayout(new BorderLayout(10, 0)); - columnHeader = new ByteViewerHeader(this); - - fm = getFontMetrics(Gui.getFont(ByteViewerComponentProvider.DEFAULT_FONT_ID)); - fontHeight = fm.getHeight(); + fontMetrics = getFontMetrics(Gui.getFont(ByteViewerComponentProvider.DEFAULT_FONT_ID)); + fontHeight = fontMetrics.getHeight(); // for the index/address column - indexFactory = new IndexFieldFactory(fm); + indexFactory = new IndexFieldFactory(fontMetrics); indexPanel = new FieldPanel(this, "Byte Viewer"); indexPanel.enableSelection(false); @@ -759,21 +669,13 @@ public class ByteViewerPanel extends JPanel indexPanel.setFocusable(false); indexPanel.addLayoutListener(this); - compPanel = new CompositePanel(indexPanel); - - scrollp = new IndexedScrollPane(compPanel); - scrollp.setWheelScrollingEnabled(false); - - columnHeader = new ByteViewerHeader(this); - columnHeader.addColumnModelListener(this); - - columnHeader.addColumn(ByteViewerComponentProvider.DEFAULT_INDEX_NAME, indexPanel); - scrollp.setColumnHeaderComp(columnHeader); - - compPanel.setBackground(new GColor("color.bg.byteviewer")); + indexedView = new ByteViewerIndexedView(indexPanel); + IndexedScrollPane indexedScrollPane = new IndexedScrollPane(indexedView); + indexedScrollPane.setWheelScrollingEnabled(false); + indexedScrollPane.setColumnHeaderComp(indexedView.getColumnHeader()); statusPanel = createStatusPanel(); - add(scrollp, BorderLayout.CENTER); + add(indexedScrollPane, BorderLayout.CENTER); add(statusPanel, BorderLayout.SOUTH); HelpService help = Help.getHelpService(); @@ -852,8 +754,7 @@ public class ByteViewerPanel extends JPanel } indexMap = new IndexMap(blockSet, bytesPerLine, blockOffset); - indexPanelWidth = getIndexPanelWidth(blocks); - indexFactory.setIndexMap(indexMap, indexPanelWidth); + indexFactory.setIndexMap(indexMap); ByteBlock block = null; BigInteger offset = BigInteger.ZERO; if (info != null) { @@ -887,7 +788,7 @@ public class ByteViewerPanel extends JPanel @Override public Dimension getPreferredViewSize() { - return new Dimension(indexPanelWidth, 500); + return new Dimension(500, 500); } @Override @@ -926,26 +827,16 @@ public class ByteViewerPanel extends JPanel } } - private int getIndexPanelWidth(ByteBlock[] blocks1) { - FontMetrics headerFm = columnHeader.getFontMetrics(columnHeader.getFont()); - String indexName = ByteViewerComponentProvider.DEFAULT_INDEX_NAME; - if (blocks1.length > 0) { - indexName = blocks1[0].getIndexName(); + private int getIndexSizeInChars() { + // set the field size at least the size of the column header name + int minChars = ByteViewerComponentProvider.INDEX_COLUMN_NAME.length(); + if (blocks != null) { + for (ByteBlock element : blocks) { + int charCount = element.getMaxLocationRepresentationSize(); + minChars = Math.max(minChars, charCount); + } } - int nameWidth = headerFm.stringWidth(indexName); - int charWidth = fm.charWidth('W'); - return Math.max(nameWidth, getMaxIndexSize() + 2 * charWidth); - } - - private int getMaxIndexSize() { - int maxWidth = 0; - int charWidth = fm.charWidth('W'); - for (ByteBlock element : blocks) { - int width = element.getMaxLocationRepresentationSize() * charWidth; - maxWidth = Math.max(maxWidth, width); - } - - return maxWidth; + return minChars; } @Override @@ -1024,175 +915,12 @@ public class ByteViewerPanel extends JPanel public void removeDisplayListener(AddressSetDisplayListener listener) { displayListeners.add(listener); } -} - -class CompositePanel extends JPanel implements IndexedScrollable, IndexScrollListener { - FieldPanel indexPanel; - BoundedRangeModel verticalScrollBarModel; - BoundedRangeModel horizontalScrollBarModel; - List viewList = new ArrayList<>(); - List allPanels = new ArrayList<>(); - private boolean processingIndexRangeChanged; - - CompositePanel(FieldPanel indexPanel) { - super(new HorizontalLayout(0)); - this.indexPanel = indexPanel; - indexPanel.addIndexScrollListener(this); - addMouseWheelListener(e -> { - // this lets us scroll the byte viewer when the user is not over any panel, but still over the view - Layout firstLayout = indexPanel.getLayoutModel().getLayout(BigInteger.ZERO); - int layoutScrollHt = firstLayout != null // - ? firstLayout.getScrollableUnitIncrement(0, 1) - : 0; - - double wheelRotation = e.getPreciseWheelRotation(); - int scrollAmount = - (int) (wheelRotation * layoutScrollHt * FieldPanel.MOUSEWHEEL_LINES_TO_SCROLL); - - indexPanel.scrollView(scrollAmount); - e.consume(); - }); - - allPanels.add(indexPanel); - rebuildPanels(); - } - - public void swapView(int fromIndex, int toIndex) { - FieldPanel from = allPanels.get(fromIndex); - FieldPanel to = allPanels.get(toIndex); - allPanels.set(fromIndex, to); - allPanels.set(toIndex, from); - rebuildPanels(); - } - - void addByteViewerComponent(ByteViewerComponent comp) { - comp.addIndexScrollListener(this); - viewList.add(comp); - allPanels.add(comp); - rebuildPanels(); - } - - void removeByteViewerComponent(ByteViewerComponent comp) { - comp.removeIndexScrollListener(this); - viewList.remove(comp); - allPanels.remove(comp); - rebuildPanels(); - } - - private void rebuildPanels() { - removeAll(); - int count = 0; - for (FieldPanel panel : allPanels) { - if (count++ != 0) { - super.add(new JSeparator(SwingConstants.VERTICAL)); - } - super.add(panel); - } -// setSize(getPreferredSize()); - invalidate(); - } - - @Override - public Component add(Component comp) { - throw new UnsupportedOperationException("External call to add(Component) not allowed"); - } - - @Override - public void remove(Component comp) { - throw new UnsupportedOperationException("External call to remove(Component) not allowed"); - } - - @Override - public void addIndexScrollListener(IndexScrollListener listener) { - indexPanel.addIndexScrollListener(listener); - } - - @Override - public int getHeight(BigInteger index) { - return indexPanel.getHeight(index); - } - - @Override - public BigInteger getIndexAfter(BigInteger index) { - return indexPanel.getIndexAfter(index); - } - - @Override - public BigInteger getIndexBefore(BigInteger index) { - return indexPanel.getIndexBefore(index); - } - - @Override - public BigInteger getIndexCount() { - return indexPanel.getIndexCount(); - } - - @Override - public boolean isUniformIndex() { - return true; - } - - @Override - public void removeIndexScrollListener(IndexScrollListener listener) { - indexPanel.removeIndexScrollListener(listener); - } - - @Override - public void scrollLineDown() { - indexPanel.scrollLineDown(); - } - - @Override - public void scrollLineUp() { - indexPanel.scrollLineUp(); - } - - @Override - public void scrollPageDown() { - indexPanel.scrollPageDown(); - } - - @Override - public void scrollPageUp() { - indexPanel.scrollPageUp(); - } - - @Override - public void showIndex(BigInteger index, int verticalOffset) { - indexPanel.showIndex(index, verticalOffset); - } - - @Override - public void indexModelChanged() { - // handled by indexPanel - } - - @Override - public void indexModelDataChanged(BigInteger start, BigInteger end) { - // handled by indexPanel - } - - @Override - public void mouseWheelMoved(double preciseWheelRotation, boolean isHorizontal) { - indexPanel.mouseWheelMoved(preciseWheelRotation, isHorizontal); - } - - @Override - public void indexRangeChanged(BigInteger startIndex, BigInteger endIndex, int yStart, - int yEnd) { - if (processingIndexRangeChanged) { - return; - } - processingIndexRangeChanged = true; - try { - // need to update all views - for (FieldPanel fieldPanel : allPanels) { - fieldPanel.showIndex(startIndex, yStart); - } - } - finally { - processingIndexRangeChanged = false; - } - } - + + public int getViewWidth(String viewName) { + return indexedView.getViewWidth(viewName); + } + + public void setViewWidth(String viewName, int width) { + indexedView.setColumnWidth(viewName, width); + } } diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/DataModelInfo.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/DataModelInfo.java deleted file mode 100644 index 04dbb19b46..0000000000 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/DataModelInfo.java +++ /dev/null @@ -1,53 +0,0 @@ -/* ### - * IP: GHIDRA - * REVIEWED: YES - * - * 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.byteviewer; - -import java.io.Serializable; - -/** - * Class used only during serialization to keep the model name and the - * group size of the model. - */ -class DataModelInfo implements Serializable { - - private String[] names; - private int[] groupSizes; - - /** - * Constructor - * @param id name of the model - * @param groupSize group size for the model - */ - DataModelInfo(int size) { - names = new String[size]; - groupSizes = new int[size]; - } - - void set(int index, String name, int groupSize) { - names[index] = name; - groupSizes[index] = groupSize; - } - - /** - * Get the name of the model. - * - * @return String - */ - String[] getNames() { - return names; - } -} diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/IndexFieldFactory.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/IndexFieldFactory.java index 2889ff4180..5f4ac0b0ca 100644 --- a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/IndexFieldFactory.java +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/IndexFieldFactory.java @@ -19,10 +19,12 @@ import java.awt.Color; import java.awt.FontMetrics; import java.math.BigInteger; +import org.apache.commons.lang3.StringUtils; + import docking.widgets.fieldpanel.field.Field; import docking.widgets.fieldpanel.field.SimpleTextField; -import docking.widgets.fieldpanel.support.Highlight; import docking.widgets.fieldpanel.support.FieldHighlightFactory; +import docking.widgets.fieldpanel.support.Highlight; import ghidra.app.plugin.core.format.ByteBlockInfo; /** @@ -30,7 +32,7 @@ import ghidra.app.plugin.core.format.ByteBlockInfo; */ class IndexFieldFactory { - private FontMetrics fm; + private FontMetrics metrics; private int width; private IndexMap indexMap; private int charWidth; @@ -41,21 +43,21 @@ class IndexFieldFactory { /** * Constructor - * @param fieldModel field model + * @param metrics the FontMetrics this field should use to do size computations */ - IndexFieldFactory(FontMetrics fm) { - this.fm = fm; + IndexFieldFactory(FontMetrics metrics) { + this.metrics = metrics; - charWidth = fm.charWidth('W'); + charWidth = metrics.charWidth('W'); width = ByteViewerComponentProvider.DEFAULT_NUMBER_OF_CHARS * charWidth; missingValueColor = ByteViewerComponentProvider.SEPARATOR_COLOR; + startX = charWidth; } /** * Gets a Field object for the given index. - * This method is called for n times where n is the number of fields - * in the model. In this case, we only have one field, so it gets - * called once for each index. + * @param index the index to get a Field for + * @return The Field for the given index */ public Field getField(BigInteger index) { @@ -74,8 +76,8 @@ class IndexFieldFactory { } if (info == null) { if (indexMap.isBlockSeparatorIndex(index)) { - SimpleTextField sf = - new SimpleTextField(noValueStr, fm, startX, width, false, highlightFactory); + SimpleTextField sf = new SimpleTextField(noValueStr, metrics, startX, width, + false, highlightFactory); sf.setForeground(missingValueColor); return sf; @@ -87,15 +89,16 @@ class IndexFieldFactory { if (locRep == null) { return null; } - return new SimpleTextField(locRep, fm, startX, width, false, highlightFactory); + return new SimpleTextField(locRep, metrics, startX, width, false, highlightFactory); } public FontMetrics getMetrics() { - return fm; + return metrics; } /** * Sets the starting x position for the fields generated by this factory + * @param x The */ public void setStartX(int x) { startX = x; @@ -103,6 +106,7 @@ class IndexFieldFactory { /** * Returns the starting x position for the fields generated by this factory. + * @return the starting x position for the fields generated by this factory. */ public int getStartX() { return startX; @@ -110,26 +114,30 @@ class IndexFieldFactory { /** * Returns the width of the fields associated with this Factory. + * @return the width of the fields associated with this Factory */ public int getWidth() { return width; } /** - * Set byte blocks. - * @param blocks + * Sets the indexMap which is the data source for the generated fields. + * @param indexMap indexMap which produces the index string for specific indexes */ - void setIndexMap(IndexMap indexMap, int maxWidth) { + void setIndexMap(IndexMap indexMap) { this.indexMap = indexMap; - width = maxWidth - (2 * charWidth); - int nchars = width / charWidth; - if (indexMap != null) { - StringBuffer sb = new StringBuffer(); - for (int i = 0; i < nchars; i++) { - sb.append("."); - } - noValueStr = sb.toString(); - } + } + + /** + * Sets the size of this field based on the number of chars to display + * @param charCount the number of chars to + */ + void setSize(int charCount) { + width = charCount * charWidth; + // add in space for margins + width += 2 * charWidth; + + noValueStr = StringUtils.repeat('.', charCount); } void setMissingValueColor(Color c) { diff --git a/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/InteractivePanelManager.java b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/InteractivePanelManager.java new file mode 100644 index 0000000000..b2b90fe96b --- /dev/null +++ b/Ghidra/Features/ByteViewer/src/main/java/ghidra/app/plugin/core/byteviewer/InteractivePanelManager.java @@ -0,0 +1,304 @@ +/* ### + * 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.byteviewer; + +import java.awt.*; +import java.beans.PropertyChangeEvent; +import java.util.ArrayList; +import java.util.List; + +import javax.swing.*; +import javax.swing.event.*; +import javax.swing.table.*; + +/** + * Creates a container for holding multiple horizontally aligned components and provides + * a {@link JTableHeader} that can be used to interactively reorder and resize the + * components that are managed by this container. + */ +public class InteractivePanelManager { + private JPanel mainPanel; + private JTableHeader header; + private TableColumnModel columnModel; + private int separatorWidth; + + public InteractivePanelManager() { + JTable table = new JTable(); + header = new JTableHeader(); + table.setTableHeader(header); + columnModel = header.getColumnModel(); + separatorWidth = (new JSeparator(SwingConstants.VERTICAL)).getPreferredSize().width; + mainPanel = new JPanel(new HeaderLayoutManager()); + columnModel.addColumnModelListener(new PanelManagerColumnModelListener()); + } + + /** + * Sets the font for the header component. + * @param font the font to be used to display view names in the header + */ + public void setHeaderFont(Font font) { + header.setFont(font); + } + + /** + * Adds a component and it's name to the set of managed components. + * @param name the name to display in its column header. + * @param component the component to add + */ + public void addComponent(String name, JComponent component) { + TableColumn column = new TableColumn(); + column.addPropertyChangeListener(e -> columnPropertyChanged(e)); + column.setHeaderValue(new ComponentData(name, component)); + column.setPreferredWidth(component.getPreferredSize().width + separatorWidth); + column.setWidth(component.getPreferredSize().width); + columnModel.addColumn(column); + mainPanel.add(component); + mainPanel.add(new JSeparator(SwingConstants.VERTICAL)); + } + + /** + * Returns a list of the components being managed. + * @return a list of the components being managed. + */ + public List getComponents() { + List components = new ArrayList<>(); + for (int i = 0; i < columnModel.getColumnCount(); i++) { + TableColumn column = columnModel.getColumn(i); + components.add(((ComponentData) column.getHeaderValue()).component); + } + return components; + } + + /** + * Sets the name for a component + * @param component the component for which to change its associated name + * @param newName the new name for the component + */ + public void setName(JComponent component, String newName) { + for (int i = 0; i < columnModel.getColumnCount(); i++) { + TableColumn column = columnModel.getColumn(i); + ComponentData componentData = (ComponentData) column.getHeaderValue(); + if (componentData.component() == component) { + column.setHeaderValue(new ComponentData(newName, component)); + break; + } + } + } + + /** + * Removes the given component from being managed. + * @param component the component to be removed + */ + public void removeComponent(JComponent component) { + + int componentCount = mainPanel.getComponentCount(); + for (int i = 0; i < componentCount; i++) { + if (mainPanel.getComponent(i) == component) { + mainPanel.remove(i); + mainPanel.remove(i); // also remove the JSeparator the follows it + break; + } + } + for (int i = 0; i < columnModel.getColumnCount(); i++) { + TableColumn column = columnModel.getColumn(i); + if (((ComponentData) column.getHeaderValue()).component == component) { + columnModel.removeColumn(column); + break; + } + } + } + + /** + * Returns the mainPanel containing the horizontally laid components. + * @return the mainPanel containing the horizontally laid components + */ + public JComponent getMainPanel() { + return mainPanel; + } + + /** + * Returns the {@link JTableHeader} component that can be used to reorder and resize + * the components + * @return the JTableHeader for managing the order and size of the components + */ + public JComponent getColumnHeader() { + return header; + } + + /** + * Returns the current width of the component with the given name. + * @param viewName the name of the component for which to get its width + * @return the current width of the component with the given name + */ + public int getColumnWidth(String viewName) { + for (int i = 0; i < columnModel.getColumnCount(); i++) { + TableColumn column = columnModel.getColumn(i); + ComponentData data = (ComponentData) column.getHeaderValue(); + if (data.name().equals(viewName)) { + return column.getWidth(); + } + } + return 0; + } + + /** + * Sets the width of the named component. + * @param viewName the name of the component to resize + * @param width the new width for the given component + */ + public void setColumnWidth(String viewName, int width) { + for (int i = 0; i < columnModel.getColumnCount(); i++) { + TableColumn column = columnModel.getColumn(i); + ComponentData data = (ComponentData) column.getHeaderValue(); + if (data.name().equals(viewName)) { + column.setWidth(width); + return; + } + } + } + + /** + * Resets the named component back to its preferred size. + * @param viewName the name of the component to restore to its preferred size + */ + public void resetColumnWidthToPreferredWidth(String viewName) { + for (int i = 0; i < columnModel.getColumnCount(); i++) { + TableColumn column = columnModel.getColumn(i); + ComponentData data = (ComponentData) column.getHeaderValue(); + if (data.name().equals(viewName)) { + column.setWidth(data.component().getPreferredSize().width); + return; + } + } + } + + private void columnPropertyChanged(PropertyChangeEvent e) { + if ("width".equals(e.getPropertyName())) { + TableColumn column = (TableColumn) e.getSource(); + column.setPreferredWidth((int) e.getNewValue()); + update(); + } + } + + private void update() { + mainPanel.invalidate(); + Container parent = mainPanel.getParent(); + if (parent != null) { + parent.validate(); + parent.repaint(); + } + } + + private class HeaderLayoutManager implements LayoutManager { + + @Override + public void addLayoutComponent(String name, Component comp) { + // don't care + } + + @Override + public void removeLayoutComponent(Component comp) { + // don't care + } + + @Override + public Dimension preferredLayoutSize(Container parent) { + Insets insets = parent.getInsets(); + int n = parent.getComponentCount(); + int height = 0; + int width = columnModel.getTotalColumnWidth(); + + for (int i = 0; i < n; i++) { + Component c = parent.getComponent(i); + Dimension d = c.getPreferredSize(); + height = Math.max(height, d.height); + } + return new Dimension(width + insets.left + insets.right, + height + insets.top + insets.bottom); + + } + + @Override + public Dimension minimumLayoutSize(Container parent) { + return preferredLayoutSize(parent); + } + + @Override + public void layoutContainer(Container parent) { + int n = parent.getComponentCount(); + Dimension d = parent.getSize(); + Insets insets = parent.getInsets(); + int height = d.height - insets.top - insets.bottom; + + int x = insets.left; + int y = insets.top; + for (int i = 0; i < n; i++) { + // even components are the actual managed components, odd components are JSeparators + if (i % 2 == 0) { + TableColumn column = columnModel.getColumn(i / 2); + ComponentData componentData = (ComponentData) column.getHeaderValue(); + JComponent c = componentData.component(); + int width = column.getWidth() - separatorWidth; + c.setBounds(x, y, width, height); + x += width; + } + else { + Component c = parent.getComponent(i); + d = c.getPreferredSize(); + c.setBounds(x, y, d.width, height); + x += d.width; + } + } + } + + } + + private class PanelManagerColumnModelListener implements TableColumnModelListener { + + @Override + public void columnAdded(TableColumnModelEvent e) { + update(); + } + + @Override + public void columnRemoved(TableColumnModelEvent e) { + update(); + } + + @Override + public void columnMoved(TableColumnModelEvent e) { + update(); + } + + @Override + public void columnMarginChanged(ChangeEvent e) { + // ignore + } + + @Override + public void columnSelectionChanged(ListSelectionEvent e) { + // ignore + } + + } + + record ComponentData(String name, JComponent component) { + public String toString() { + return name; + } + } + +} diff --git a/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/app/plugin/core/byteviewer/ByteViewerPlugin1Test.java b/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/app/plugin/core/byteviewer/ByteViewerPlugin1Test.java index bd52ae8b50..e5abd211ce 100644 --- a/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/app/plugin/core/byteviewer/ByteViewerPlugin1Test.java +++ b/Ghidra/Features/ByteViewer/src/test.slow/java/ghidra/app/plugin/core/byteviewer/ByteViewerPlugin1Test.java @@ -24,7 +24,8 @@ import java.util.List; import javax.swing.JCheckBox; import javax.swing.JLabel; -import javax.swing.event.TableColumnModelEvent; +import javax.swing.table.JTableHeader; +import javax.swing.table.TableColumnModel; import org.junit.*; @@ -167,10 +168,9 @@ public class ByteViewerPlugin1Test extends AbstractGhidraHeadedIntegrationTest { assertEquals(3, panel.getNumberOfViews()); - DataModelInfo info = panel.getDataModelInfo(); - String[] names = info.getNames(); - assertEquals(3, names.length); - Set viewNames = new HashSet<>(Arrays.asList(names)); + List names = panel.getViewNamesInDisplayOrder(); + assertEquals(3, names.size()); + Set viewNames = new HashSet<>(names); assertTrue(viewNames.contains("Hex")); assertTrue(viewNames.contains("Octal")); assertTrue(viewNames.contains("Ascii")); @@ -196,10 +196,9 @@ public class ByteViewerPlugin1Test extends AbstractGhidraHeadedIntegrationTest { assertEquals(1, panel.getNumberOfViews()); - info = panel.getDataModelInfo(); - names = info.getNames(); - assertEquals(1, names.length); - assertEquals("Octal", names[0]); + names = panel.getViewNamesInDisplayOrder(); + assertEquals(1, names.size()); + assertEquals("Octal", names.get(0)); } @@ -618,56 +617,73 @@ public class ByteViewerPlugin1Test extends AbstractGhidraHeadedIntegrationTest { @Test public void testReorderViews() throws Exception { loadViews("Ascii", "Octal"); - final ByteViewerHeader columnHeader = - (ByteViewerHeader) findContainer(panel, ByteViewerHeader.class); + + List names = panel.getViewNamesInDisplayOrder(); + assertEquals("Hex", names.get(0)); + assertEquals("Octal", names.get(1)); + assertEquals("Ascii", names.get(2)); + + final JTableHeader columnHeader = (JTableHeader) findContainer(panel, JTableHeader.class); // move column 3 to 2 runSwing(() -> { - TableColumnModelEvent ev = - new TableColumnModelEvent(columnHeader.getColumnModel(), 3, 2); - panel.columnMoved(ev); + TableColumnModel columnModel = columnHeader.getColumnModel(); + columnModel.moveColumn(3, 2); }); - String[] names = panel.getDataModelInfo().getNames(); + names = panel.getViewNamesInDisplayOrder(); + assertEquals("Hex", names.get(0)); + assertEquals("Ascii", names.get(1)); + assertEquals("Octal", names.get(2)); - // move column 1 to 0 + // move column 2 to 1 runSwing(() -> { - TableColumnModelEvent ev = - new TableColumnModelEvent(columnHeader.getColumnModel(), 2, 1); - panel.columnMoved(ev); + TableColumnModel columnModel = columnHeader.getColumnModel(); + columnModel.moveColumn(2, 1); }); - String[] newNames = panel.getDataModelInfo().getNames(); - assertEquals(names[0], newNames[1]); - assertEquals(names[1], newNames[0]); - assertEquals(names[2], newNames[2]); + names = panel.getViewNamesInDisplayOrder(); + assertEquals("Ascii", names.get(0)); + assertEquals("Hex", names.get(1)); + assertEquals("Octal", names.get(2)); } + @Test + public void testResizeViews() { + loadViews("Ascii", "Octal"); + assertNotEquals(200, getViewWidth("Ascii")); + + setViewWidth("Ascii", 200); + + assertEquals(200, panel.getViewWidth("Ascii")); + } + + @Test + public void testResizeViewsSaveState() { + loadViews("Ascii", "Octal"); + assertNotEquals(200, getViewWidth("Ascii")); + + setViewWidth("Ascii", 200); + env.saveRestoreToolState(); + + assertEquals(200, panel.getViewWidth("Ascii")); + } + @Test public void testReorderViewsSaveState() throws Exception { loadViews("Ascii", "Octal"); - final ByteViewerHeader columnHeader = - (ByteViewerHeader) findContainer(panel, ByteViewerHeader.class); - // move column 3 to 2 - runSwing(() -> { - TableColumnModelEvent ev = - new TableColumnModelEvent(columnHeader.getColumnModel(), 3, 2); - panel.columnMoved(ev); - }); + final JTableHeader columnHeader = (JTableHeader) findContainer(panel, JTableHeader.class); // move column 1 to 0 runSwing(() -> { - TableColumnModelEvent ev = - new TableColumnModelEvent(columnHeader.getColumnModel(), 1, 0); - panel.columnMoved(ev); + TableColumnModel columnModel = columnHeader.getColumnModel(); + columnModel.moveColumn(3, 2); }); - String[] names = panel.getDataModelInfo().getNames(); + List names = panel.getViewNamesInDisplayOrder(); env.saveRestoreToolState(); - String[] newNames = panel.getDataModelInfo().getNames(); - for (int i = 0; i < names.length; i++) { - assertEquals(names[i], newNames[i]); - } + List newNames = panel.getViewNamesInDisplayOrder(); + assertEquals(names, newNames); } @Test @@ -861,8 +877,8 @@ public class ByteViewerPlugin1Test extends AbstractGhidraHeadedIntegrationTest { } private Address convertToAddr(ByteBlockInfo info) { - return ((ProgramByteBlockSet) plugin.getProvider().getByteBlockSet()).getAddress( - info.getBlock(), info.getOffset()); + return ((ProgramByteBlockSet) plugin.getProvider().getByteBlockSet()) + .getAddress(info.getBlock(), info.getOffset()); } private boolean byteBlockSelectionEquals(ByteBlockSelection b1, ByteBlockSelection b2) { @@ -942,4 +958,15 @@ public class ByteViewerPlugin1Test extends AbstractGhidraHeadedIntegrationTest { return null; } + private void setViewWidth(String name, int width) { + runSwing(() -> { + panel.setViewWidth(name, width); + }); + } + + private int getViewWidth(String name) { + return runSwing(() -> { + return panel.getViewWidth("Ascii"); + }); + } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/indexedscrollpane/IndexedScrollable.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/indexedscrollpane/IndexedScrollable.java index 94157daddf..a8a90db072 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/indexedscrollpane/IndexedScrollable.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/indexedscrollpane/IndexedScrollable.java @@ -103,4 +103,5 @@ public interface IndexedScrollable { * @param isHorizontal true if the rotation was horizontal, false for vertical */ public void mouseWheelMoved(double preciseWheelRotation, boolean isHorizontal); + }