diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/DirectoryList.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/DirectoryList.java index 3230d625d7..e2a16741b4 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/DirectoryList.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/DirectoryList.java @@ -25,6 +25,8 @@ import java.util.ArrayList; import java.util.List; import javax.swing.*; +import javax.swing.event.ListDataEvent; +import javax.swing.event.ListDataListener; import docking.event.mouse.GMouseListenerAdapter; import docking.widgets.label.GDLabel; @@ -32,9 +34,13 @@ import docking.widgets.list.GList; import ghidra.util.exception.AssertException; class DirectoryList extends GList implements GhidraFileChooserDirectoryModelIf { + private static final int DEFAULT_ICON_SIZE = 16; + private static final int WIDTH_PADDING = 14; + private static final int HEIGHT_PADDING = 5; private GhidraFileChooser chooser; private DirectoryListModel model; + private FileListCellRenderer cellRenderer; private JLabel listEditorLabel; private JTextField listEditorField; private JPanel listEditor; @@ -52,18 +58,34 @@ class DirectoryList extends GList implements GhidraFileChooserDirectoryMod private void build() { setLayoutOrientation(JList.VERTICAL_WRAP); - setCellRenderer(new FileListCellRenderer(chooser)); - - addMouseListener(new MouseAdapter() { + setCellRenderer((cellRenderer = new FileListCellRenderer(getFont(), chooser))); + model.addListDataListener(new ListDataListener() { @Override - public void mouseClicked(MouseEvent e) { - // always end editing on a mouse click of any kind - listEditor.setVisible(false); - requestFocus(); + public void contentsChanged(ListDataEvent e) { + // called when the list changes because a new file is inserted (ie. create new folder action) + recomputeListCellDimensions(null); + } + + @Override + public void intervalAdded(ListDataEvent e) { + recomputeListCellDimensions(null); + } + + @Override + public void intervalRemoved(ListDataEvent e) { + // don't care } }); addMouseListener(new GMouseListenerAdapter() { + @Override + public void mouseClicked(MouseEvent e) { + super.mouseClicked(e); + + // always end editing on a mouse click of any kind + listEditor.setVisible(false); + requestFocus(); + } @Override public boolean shouldConsume(MouseEvent e) { @@ -156,7 +178,15 @@ class DirectoryList extends GList implements GhidraFileChooserDirectoryMod e.consume(); } else if (e.getKeyCode() == KeyEvent.VK_ENTER) { - stopListEdit(); + String invalidFilenameMessage = + chooser.getInvalidFilenameMessage(listEditorField.getText()); + if (invalidFilenameMessage != null) { + chooser.setStatusText(invalidFilenameMessage); + // keep the user in the field by not stopping the current edit + } + else { + stopListEdit(); + } e.consume(); } } @@ -281,36 +311,6 @@ class DirectoryList extends GList implements GhidraFileChooserDirectoryMod setSelectedIndices(indices); } - // overridden to account for the renderers insets, to avoid clipping - @Override - public void setFixedCellWidth(int width) { - - int fullWidth = width; - ListCellRenderer renderer = getCellRenderer(); - if (renderer instanceof JComponent) { - JComponent c = (JComponent) renderer; - Insets insets = c.getInsets(); - fullWidth += insets.left + insets.right; - } - - super.setFixedCellWidth(fullWidth); - } - - // overridden to account for the renderers insets, to avoid clipping - @Override - public void setFixedCellHeight(int height) { - - int fullHeight = height; - ListCellRenderer renderer = getCellRenderer(); - if (renderer instanceof JComponent) { - JComponent c = (JComponent) renderer; - Insets insets = c.getInsets(); - fullHeight += insets.top + insets.bottom; - } - - super.setFixedCellHeight(fullHeight); - } - private boolean isEditing() { return (editedFile != null); } @@ -350,6 +350,14 @@ class DirectoryList extends GList implements GhidraFileChooserDirectoryMod return; } + String invalidFilenameMessage = + chooser.getInvalidFilenameMessage(listEditorField.getText()); + if (invalidFilenameMessage != null) { + chooser.setStatusText("Rename aborted - " + invalidFilenameMessage); + cancelListEdit(); + return; + } + File editedFileCopy = editedFile; int index = model.indexOfFile(editedFileCopy); if (index < 0) { @@ -358,6 +366,7 @@ class DirectoryList extends GList implements GhidraFileChooserDirectoryMod File dest = new File(editedFileCopy.getParentFile(), listEditorField.getText()); cancelListEdit(); if (chooser.getModel().renameFile(editedFileCopy, dest)) { + chooser.setStatusText(""); model.set(index, dest); //chooser.updateFiles(chooser.getCurrentDirectory(), true); chooser.setSelectedFileAndUpdateDisplay(dest); @@ -367,6 +376,36 @@ class DirectoryList extends GList implements GhidraFileChooserDirectoryMod } } + /** + * Resizes this list's cell dimensions based on the string widths found in the supplied + * list of files. + *

+ * If there there are no files, uses the JScrollPane that contains us for the cellwidth. + * + * @param files list of files to use to resize the list's fixed cell dimensions. If null, uses + * the model's current set of files. + */ + private void recomputeListCellDimensions(List files) { + files = (files != null) ? files : model.getAllFiles(); + Dimension d = + cellRenderer.computePlainTextListCellDimensions(this, files, 0, DEFAULT_ICON_SIZE); + if (d.width == 0 && getParent() != null) { + // special case: if there were no files to measure, use the containing JScrollPane's + // width + if (getParent().getParent() instanceof JScrollPane) { + JScrollPane parent = (JScrollPane) getParent().getParent(); + Dimension parentSize = parent.getSize(); + Insets insets = parent.getInsets(); + d.width = parentSize.width - (insets != null ? insets.right + insets.left : 0); + } + } + else { + d.width += DEFAULT_ICON_SIZE + WIDTH_PADDING; + } + setFixedCellWidth(d.width); + setFixedCellHeight(d.height + HEIGHT_PADDING); + } + /*junit*/ JTextField getListEditorText() { return listEditorField; } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/DirectoryListModel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/DirectoryListModel.java index 54547ca800..4f2a4907eb 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/DirectoryListModel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/DirectoryListModel.java @@ -74,6 +74,10 @@ class DirectoryListModel extends AbstractListModel { return fileList.indexOf(file); } + public List getAllFiles() { + return new ArrayList<>(fileList); + } + @Override public int getSize() { return fileList.size(); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/FileEditor.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/FileEditor.java index 4968df99a9..7675176475 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/FileEditor.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/FileEditor.java @@ -63,6 +63,14 @@ class FileEditor extends AbstractCellEditor implements TableCellEditor { directoryTable.editingCanceled(new ChangeEvent(FileEditor.this)); e.consume(); } + else if (e.getKeyCode() == KeyEvent.VK_ENTER) { + String invalidFilenameMessage = + chooser.getInvalidFilenameMessage(nameField.getText()); + if (invalidFilenameMessage != null) { + chooser.setStatusText(invalidFilenameMessage); + e.consume(); + } + } } @Override @@ -72,7 +80,14 @@ class FileEditor extends AbstractCellEditor implements TableCellEditor { e.consume(); } else if (e.getKeyCode() == KeyEvent.VK_ENTER) { - directoryTable.editingStopped(new ChangeEvent(FileEditor.this)); + String invalidFilenameMessage = + chooser.getInvalidFilenameMessage(nameField.getText()); + if (invalidFilenameMessage != null) { + chooser.setStatusText(invalidFilenameMessage); + } + else { + directoryTable.editingStopped(new ChangeEvent(FileEditor.this)); + } e.consume(); } } @@ -145,6 +160,11 @@ class FileEditor extends AbstractCellEditor implements TableCellEditor { } private File getNewFile() { + String invalidFilenameMessage = chooser.getInvalidFilenameMessage(nameField.getText()); + if (invalidFilenameMessage != null) { + chooser.setStatusText("Rename aborted - " + invalidFilenameMessage); + return originalFile; + } GhidraFileChooserModel fileChooserModel = chooser.getModel(); File newFile = new GhidraFile(originalFile.getParentFile(), nameField.getText(), fileChooserModel.getSeparator()); diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/FileListCellRenderer.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/FileListCellRenderer.java index 714cec4134..d6a7eb0bae 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/FileListCellRenderer.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/FileListCellRenderer.java @@ -16,6 +16,7 @@ package docking.widgets.filechooser; import java.awt.Component; +import java.awt.Font; import java.io.File; import javax.swing.JList; @@ -28,9 +29,11 @@ class FileListCellRenderer extends GListCellRenderer { private GhidraFileChooser chooser; private GhidraFileChooserModel model; - public FileListCellRenderer(GhidraFileChooser chooser) { + public FileListCellRenderer(Font font, GhidraFileChooser chooser) { + super(font); this.chooser = chooser; this.model = chooser.getModel(); + setShouldAlternateRowBackgroundColors(false); } @Override diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/GhidraFileChooser.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/GhidraFileChooser.java index c6e24750a2..e247a1571a 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/GhidraFileChooser.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/GhidraFileChooser.java @@ -22,22 +22,23 @@ import java.io.FileFilter; import java.text.SimpleDateFormat; import java.util.*; import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import java.util.stream.Collectors; import javax.swing.*; -import javax.swing.border.Border; -import javax.swing.event.*; +import javax.swing.event.CellEditorListener; +import javax.swing.event.ChangeEvent; import javax.swing.filechooser.FileSystemView; import docking.*; -import docking.framework.DockingApplicationConfiguration; import docking.widgets.*; import docking.widgets.combobox.GComboBox; import docking.widgets.label.GDLabel; import docking.widgets.label.GLabel; import docking.widgets.list.GListCellRenderer; -import ghidra.GhidraApplicationLayout; -import ghidra.framework.*; +import ghidra.framework.OperatingSystem; +import ghidra.framework.Platform; import ghidra.framework.preferences.Preferences; import ghidra.util.Msg; import ghidra.util.SystemUtilities; @@ -86,11 +87,9 @@ public class GhidraFileChooser extends DialogComponentProvider static final String DOT = "."; static final String DOTDOT = ".."; static final String NEW_FOLDER = "New Folder"; + static final Pattern INVALID_FILENAME_PATTERN = Pattern.compile("[/\\\\*?]"); private static final int PAD = 5; - private static final int DEFAULT_ICON_SIZE = 16; - private static final int HEIGHT_PADDING = 5; - private static final int WIDTH_PADDING = 14; private static Icon refreshIcon = Icons.REFRESH_ICON; private static Icon backIcon = ResourceManager.loadImage("images/left.png"); @@ -554,31 +553,6 @@ public class GhidraFileChooser extends DialogComponentProvider private JScrollPane buildDirectoryList() { directoryListModel = new DirectoryListModel(); - - // SCR 3392 - 12/7/07 - // initially added to resize the cells of the list when the new files are - // inserted into the list (like when creating a single new folder) - directoryListModel.addListDataListener(new ListDataListener() { - @Override - public void contentsChanged(ListDataEvent e) { - int size = directoryListModel.getSize(); - List fileList = new ArrayList<>(size); - for (int i = 0; i < size; i++) { - fileList.add(directoryListModel.getFile(i)); - } - computeListCellDimensions(fileList); - } - - @Override - public void intervalAdded(ListDataEvent e) { - // don't care - } - - @Override - public void intervalRemoved(ListDataEvent e) { - // don't care - } - }); directoryList = new DirectoryList(this, directoryListModel); directoryList.setName("LIST"); @@ -850,42 +824,6 @@ public class GhidraFileChooser extends DialogComponentProvider return worker.isBusy(); } - private void computeListCellDimensions(List files) { - - Font font = directoryList.getFont(); - FontMetrics metrics = directoryList.getFontMetrics(font); - - int maxWidth = getDefaultMaxWidth(files, metrics); - - directoryList.setFixedCellWidth(maxWidth + DEFAULT_ICON_SIZE + WIDTH_PADDING); - int rowHeight = Math.max(metrics.getHeight(), DEFAULT_ICON_SIZE); - directoryList.setFixedCellHeight(rowHeight + HEIGHT_PADDING); - } - - private int getDefaultMaxWidth(List theFiles, FontMetrics metrics) { - int maxWidth = 0; - if (theFiles.size() > 0) { - for (File file : theFiles) { - maxWidth = Math.max(maxWidth, metrics.stringWidth(getDisplayName(file))); - } - return maxWidth; - } - - Dimension scrollSize = directoryScroll.getSize(); - if (scrollSize.width == 0) { - return 0; - } - - Border border = directoryScroll.getBorder(); - if (border != null) { - Insets borderInsets = border.getBorderInsets(directoryScroll); - if (borderInsets != null) { - scrollSize.width -= (borderInsets.right + borderInsets.left); - } - } - return scrollSize.width - WIDTH_PADDING - DEFAULT_ICON_SIZE; - } - String getDisplayName(File file) { if (file != null) { if (GhidraFileChooser.MY_COMPUTER.equals(getCurrentDirectory())) { @@ -906,8 +844,9 @@ public class GhidraFileChooser extends DialogComponentProvider } private void setDirectoryList(File directory, List files) { + // if the visible listing is still the same directory as this incoming list of files if (currentDirectory().equals(directory)) { - computeListCellDimensions(files); + // recompute list cell dims before causing an update to the model directoryTableModel.setFiles(files); directoryTable.scrollRectToVisible(new Rectangle(0, 0, 0, 0)); directoryListModel.setFiles(files); @@ -2174,12 +2113,17 @@ public class GhidraFileChooser extends DialogComponentProvider } } - public static void main(String[] args) throws Exception { - - GhidraApplicationLayout layout = new GhidraApplicationLayout(); - Application.initializeApplication(layout, new DockingApplicationConfiguration()); - GhidraFileChooser chooser = new GhidraFileChooser(null); - chooser.show(); - System.exit(0); + String getInvalidFilenameMessage(String filename) { + switch (filename) { + case ".": + case "..": + return "Reserved name '" + filename + "'"; + default: + Matcher m = GhidraFileChooser.INVALID_FILENAME_PATTERN.matcher(filename); + if (m.find()) { + return "Invalid characters: " + m.group(); + } + } + return null; } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/list/GListCellRenderer.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/list/GListCellRenderer.java index ac052c9ecb..16cb5bf4fe 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/list/GListCellRenderer.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/list/GListCellRenderer.java @@ -16,6 +16,7 @@ package docking.widgets.list; import java.awt.*; +import java.util.List; import java.util.function.Function; import javax.swing.*; @@ -121,4 +122,31 @@ public class GListCellRenderer extends AbstractGCellRenderer implements ListC protected void configureFont(JList list, ListModel model, int index) { setFont(defaultFont); } + + /** + * Returns the width, height necessary to display the largest element in this list. + *

+ * Useful for setting a JList's fixed cell width and height to the actual necessary size. + *

+ * NOTE: the items and the renderer must be in plain text mode, not HTML rendering mode. + * + * @param list the JList that uses this cell renderer + * @param items the items to measure + * @param minWidth the minimum width that can be returned + * @param minHeight the minimum height that can be returned + * @return a new Dimension containing a width and height value necessary to display the largest + * element in the list + */ + public Dimension computePlainTextListCellDimensions(JList list, + List items, + int minWidth, int minHeight) { + configureFont(list, list.getModel(), 0); + FontMetrics metrics = getFontMetrics(getFont()); + int maxWidth = minWidth; + for (E item : items) { + String text = getItemText(item).toString(); + maxWidth = Math.max(maxWidth, metrics.stringWidth(text)); + } + return new Dimension(maxWidth, Math.max(metrics.getHeight(), minHeight)); + } }