GP-2059 improve GhidraFileChooser interactivity

Refactor how file system root locations are handled to avoid potential slowdowns and swing thread blocking.
This commit is contained in:
dev747368 2022-06-02 14:57:58 -04:00
parent b6501c8283
commit c99f770b23
8 changed files with 287 additions and 273 deletions

View File

@ -16,12 +16,13 @@
package ghidra.plugins.fsbrowser;
import static ghidra.formats.gfilesystem.fileinfo.FileAttributeType.*;
import static java.util.Map.*;
import static java.util.Map.entry;
import java.util.*;
import java.util.function.Function;
import java.awt.Component;
import java.io.*;
import java.util.*;
import java.util.function.Function;
import javax.swing.*;
@ -109,9 +110,6 @@ class FSBActionManager {
this.textEditorService = textEditorService;
this.gTree = gTree;
chooserExport = new GhidraFileChooser(provider.getComponent());
chooserExportAll = new GhidraFileChooser(provider.getComponent());
createActions();
}
@ -380,11 +378,14 @@ class FSBActionManager {
if (fsrl == null) {
return;
}
if (chooserExport == null) {
chooserExport = new GhidraFileChooser(provider.getComponent());
chooserExport.setFileSelectionMode(GhidraFileChooserMode.FILES_ONLY);
chooserExport.setTitle("Select Where To Export File");
chooserExport.setApproveButtonText("Export");
}
File selectedFile =
new File(chooserExport.getCurrentDirectory(), fsrl.getName());
chooserExport.setFileSelectionMode(GhidraFileChooserMode.FILES_ONLY);
chooserExport.setTitle("Select Where To Export File");
chooserExport.setApproveButtonText("Export");
chooserExport.setSelectedFile(selectedFile);
File outputFile = chooserExport.getSelectedFile();
if (outputFile == null) {
@ -421,11 +422,13 @@ class FSBActionManager {
if (fsrl instanceof FSRLRoot) {
fsrl = fsrl.appendPath("/");
}
chooserExportAll
.setFileSelectionMode(GhidraFileChooserMode.DIRECTORIES_ONLY);
chooserExportAll.setTitle("Select Export Directory");
chooserExportAll.setApproveButtonText("Export All");
if (chooserExportAll == null) {
chooserExportAll = new GhidraFileChooser(provider.getComponent());
chooserExportAll
.setFileSelectionMode(GhidraFileChooserMode.DIRECTORIES_ONLY);
chooserExportAll.setTitle("Select Export Directory");
chooserExportAll.setApproveButtonText("Export All");
}
chooserExportAll.setSelectedFile(null);
File outputFile = chooserExportAll.getSelectedFile();
if (outputFile == null) {

View File

@ -15,9 +15,10 @@
*/
package docking.widgets.filechooser;
import java.io.File;
import java.util.*;
import java.io.File;
import docking.widgets.table.AbstractSortedTableModel;
class DirectoryTableModel extends AbstractSortedTableModel<File> {
@ -28,7 +29,7 @@ class DirectoryTableModel extends AbstractSortedTableModel<File> {
final static int TIME_COL = 2;
private GhidraFileChooser chooser;
private File[] files = new File[0];
private List<File> files = new ArrayList<>();
DirectoryTableModel(GhidraFileChooser chooser) {
super(FILE_COL);
@ -36,31 +37,27 @@ class DirectoryTableModel extends AbstractSortedTableModel<File> {
}
void insert(File file) {
int len = files.length;
File[] arr = new File[len + 1];
System.arraycopy(files, 0, arr, 0, len);
arr[len] = file;
files = arr;
int len = files.size();
files.add(file);
fireTableRowsInserted(len, len);
}
void setFiles(List<File> fileList) {
this.files = new File[fileList.size()];
files = fileList.toArray(files);
System.arraycopy(files, 0, this.files, 0, files.length);
files.clear();
files.addAll(fileList);
fireTableDataChanged();
}
File getFile(int row) {
if (row >= 0 && row < files.length) {
return files[row];
if (row >= 0 && row < files.size()) {
return files.get(row);
}
return null;
}
void setFile(int row, File file) {
if (row >= 0 && row < files.length) {
files[row] = file;
if (row >= 0 && row < files.size()) {
files.set(row, file);
fireTableRowsUpdated(row, row);
}
}
@ -72,7 +69,7 @@ class DirectoryTableModel extends AbstractSortedTableModel<File> {
@Override
public int getRowCount() {
return files == null ? 0 : files.length;
return files.size();
}
@Override
@ -129,7 +126,7 @@ class DirectoryTableModel extends AbstractSortedTableModel<File> {
@Override
public List<File> getModelData() {
return Arrays.asList(files);
return files;
}
@Override
@ -140,7 +137,7 @@ class DirectoryTableModel extends AbstractSortedTableModel<File> {
@Override
public void setValueAt(Object aValue, int row, int column) {
if (row < 0 || row >= files.length) {
if (row < 0 || row >= files.size()) {
return;
}
@ -150,7 +147,7 @@ class DirectoryTableModel extends AbstractSortedTableModel<File> {
switch (column) {
case FILE_COL:
files[row] = (File) aValue;
files.set(row, (File) aValue);
update();
break;
}

View File

@ -1,6 +1,5 @@
/* ###
* 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.
@ -19,10 +18,11 @@
*/
package docking.widgets.filechooser;
import ghidra.util.filechooser.GhidraFileChooserModel;
import java.util.Comparator;
import java.io.File;
import java.util.Comparator;
import ghidra.util.filechooser.GhidraFileChooserModel;
class FileComparator implements Comparator<File> {
final static int SORT_BY_NAME = 1111;
@ -59,7 +59,7 @@ class FileComparator implements Comparator<File> {
else if (sortBy == SORT_BY_TIME) {
if (model.isDirectory(file1)) {
if (model.isDirectory(file2)) {
return compare(file1.lastModified(), file2.lastModified());
return Long.compare(file1.lastModified(), file2.lastModified());
}
return -1; // dirs come before files
}
@ -73,24 +73,11 @@ class FileComparator implements Comparator<File> {
value = file1.getName().compareToIgnoreCase(file2.getName());
}
else if (sortBy == SORT_BY_SIZE) {
value = compare(file1.length(), file2.length());
value = Long.compare(file1.length(), file2.length());
}
else if (sortBy == SORT_BY_TIME) {
value = compare(file1.lastModified(), file2.lastModified());
value = Long.compare(file1.lastModified(), file2.lastModified());
}
return value;
}
private int compare(long l1, long l2) {
if (l1 == l2) {
return 0;
}
if (l1 - l2 > 0) {
return 1;
}
return -1;
}
}

View File

@ -42,8 +42,10 @@ import ghidra.framework.Platform;
import ghidra.framework.preferences.Preferences;
import ghidra.util.*;
import ghidra.util.exception.AssertException;
import ghidra.util.filechooser.*;
import ghidra.util.filechooser.GhidraFileChooserModel;
import ghidra.util.filechooser.GhidraFileFilter;
import ghidra.util.layout.PairLayout;
import ghidra.util.task.SwingUpdateManager;
import ghidra.util.task.TaskMonitor;
import ghidra.util.worker.Job;
import ghidra.util.worker.Worker;
@ -68,8 +70,7 @@ import util.HistoryList;
* <li>This class provides shortcut buttons similar to those of the Windows native chooser</li>
* </ol>
*/
public class GhidraFileChooser extends DialogComponentProvider
implements GhidraFileChooserListener, FileFilter {
public class GhidraFileChooser extends DialogComponentProvider implements FileFilter {
static final String UP_BUTTON_NAME = "UP_BUTTON";
private static final Color FOREROUND_COLOR = Color.BLACK;
@ -198,6 +199,7 @@ public class GhidraFileChooser extends DialogComponentProvider
private boolean wasCancelled;
private boolean multiSelectionEnabled;
private FileChooserActionManager actionManager;
private SwingUpdateManager modelUpdater = new SwingUpdateManager(this::updateDirectoryModels);
/**
* The last input component to take focus (the text field or file view).
@ -243,7 +245,7 @@ public class GhidraFileChooser extends DialogComponentProvider
private void init(GhidraFileChooserModel newModel) {
this.fileChooserModel = newModel;
this.fileChooserModel.setListener(this);
this.fileChooserModel.setModelUpdateCallback(modelUpdater::update);
history.setAllowDuplicates(true);
@ -410,10 +412,9 @@ public class GhidraFileChooser extends DialogComponentProvider
filterCombo = new GComboBox<>();
filterCombo.setRenderer(GListCellRenderer.createDefaultCellTextRenderer(
fileFilter -> fileFilter != null ? fileFilter.getDescription() : ""));
filterCombo.addItemListener(e -> rescanCurrentDirectory());
filterModel = (DefaultComboBoxModel<GhidraFileFilter>) filterCombo.getModel();
addFileFilter(GhidraFileFilter.ALL);
filterCombo.addItemListener(e -> rescanCurrentDirectory());
JPanel filenamePanel = new JPanel(new PairLayout(PAD, PAD));
filenamePanel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
@ -607,12 +608,9 @@ public class GhidraFileChooser extends DialogComponentProvider
// End Setup Methods
//==================================================================================================
@Override
public void modelChanged() {
SystemUtilities.runSwingLater(() -> {
directoryListModel.update();
directoryTableModel.update();
});
private void updateDirectoryModels() {
directoryListModel.update();
directoryTableModel.update();
}
@Override
@ -2003,8 +2001,8 @@ public class GhidraFileChooser extends DialogComponentProvider
return;
}
File[] files = fileChooserModel.getListing(directory, GhidraFileChooser.this);
loadedFiles = Arrays.asList(files);
loadedFiles =
new ArrayList<>(fileChooserModel.getListing(directory, GhidraFileChooser.this));
Collections.sort(loadedFiles, new FileComparator(fileChooserModel));
}
@ -2019,26 +2017,30 @@ public class GhidraFileChooser extends DialogComponentProvider
private class UpdateMyComputerJob extends FileChooserJob {
private final File myComputerFile;
private final boolean addToHistory;
private final boolean forceUpdate;
private List<File> roots;
public UpdateMyComputerJob(File myComputerFile, boolean forceUpdate, boolean addToHistory) {
this.myComputerFile = myComputerFile;
this.addToHistory = addToHistory;
this.forceUpdate = forceUpdate;
setCurrentDirectoryDisplay(myComputerFile, addToHistory);
setWaitPanelVisible(true);
}
@Override
public void run() {
roots = Arrays.asList(fileChooserModel.getRoots(forceUpdate));
if (fileChooserModel == null) {
return;
}
roots = new ArrayList<>(fileChooserModel.getRoots(forceUpdate));
Collections.sort(roots);
}
@Override
public void runSwing() {
setCurrentDirectoryDisplay(myComputerFile, addToHistory);
setDirectoryList(myComputerFile, roots);
setWaitPanelVisible(false);
Swing.runLater(() -> doSetSelectedFileAndUpdateDisplay(null));
}
}

View File

@ -15,8 +15,9 @@
*/
package docking.widgets.filechooser;
import java.util.HashMap;
import java.util.Map;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import java.io.File;
import java.io.FileFilter;
@ -25,66 +26,49 @@ import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.filechooser.FileSystemView;
import ghidra.util.filechooser.GhidraFileChooserListener;
import ghidra.util.filechooser.GhidraFileChooserModel;
import resources.ResourceManager;
import utility.function.Callback;
/**
* A default implementation of the file chooser model
* that browses the local file system.
* A default implementation of the file chooser model that browses the local file system.
*
*/
public class LocalFileChooserModel implements GhidraFileChooserModel {
private static final ImageIcon PROBLEM_FILE_ICON =
ResourceManager.loadImage("images/unknown.gif");
private static final ImageIcon PENDING_ROOT_ICON =
ResourceManager.loadImage("images/famfamfam_silk_icons_v013/drive.png");
private FileSystemView fsView = FileSystemView.getFileSystemView();
private Map<File, String> rootDescripMap = new HashMap<>();
private Map<File, Icon> rootIconMap = new HashMap<>();
private static final FileSystemRootInfo FS_ROOT_INFO = new FileSystemRootInfo();
private static final FileSystemView FS_VIEW = FileSystemView.getFileSystemView();
/**
* This is a cache of file icons, as returned from the OS's file icon service.
* <p>
* This cache is cleared each time a directory is requested (via {@link #getListing(File, FileFilter)}
* so that any changes to a file's icon are visible the next time the user hits
* refresh or navigates into a directory.
* This cache is cleared each time a directory is requested (via
* {@link #getListing(File, FileFilter)} so that any changes to a file's icon are visible the
* next time the user hits refresh or navigates into a directory.
*/
private Map<File, Icon> fileIconMap = new HashMap<>();
private File[] roots = new File[0];
private GhidraFileChooserListener listener;
private Callback callback;
/**
* @see ghidra.util.filechooser.GhidraFileChooserModel#getSeparator()
*/
@Override
public char getSeparator() {
return File.separatorChar;
}
/**
* @see ghidra.util.filechooser.GhidraFileChooserModel#setListener(ghidra.util.filechooser.GhidraFileChooserListener)
*/
@Override
public void setListener(GhidraFileChooserListener l) {
this.listener = l;
public void setModelUpdateCallback(Callback callback) {
this.callback = callback;
}
/**
* @see ghidra.util.filechooser.GhidraFileChooserModel#getHomeDirectory()
*/
@Override
public File getHomeDirectory() {
return new File(System.getProperty("user.home"));
}
/**
* Probes for a "Desktop" directory under the user's home directory.
* <p>
* Returns null if the desktop directory is missing.
* <p>
* @see ghidra.util.filechooser.GhidraFileChooserModel#getDesktopDirectory()
*/
@Override
public File getDesktopDirectory() {
String userHomeProp = System.getProperty("user.home");
@ -99,118 +83,41 @@ public class LocalFileChooserModel implements GhidraFileChooserModel {
}
@Override
public File[] getRoots(boolean forceUpdate) {
if (roots.length == 0 || forceUpdate) {
roots = File.listRoots();
// pre-populate root Description cache mapping with placeholder values that will be
// overwritten by the background thread.
synchronized (rootDescripMap) {
for (File r : roots) {
rootDescripMap.put(r, getFastRootDescriptionString(r));
rootIconMap.put(r, fsView.getSystemIcon(r));
}
}
Thread backgroundRootScanThread = new FileDescriptionThread();
backgroundRootScanThread.start();
public List<File> getRoots(boolean forceUpdate) {
if (FS_ROOT_INFO.isEmpty() || forceUpdate) {
FS_ROOT_INFO.updateRootInfo(callback);
}
return roots;
return FS_ROOT_INFO.getRoots();
}
/**
* Return a description string for a file system root. Avoid slow calls (such as {@link FileSystemView#getSystemDisplayName(File)}.
* <p>
* Used when pre-populating the root description map with values before {@link FileDescriptionThread background thread}
* finishes.
*/
protected String getFastRootDescriptionString(File root) {
String fsvSTD = "Unknown status";
try {
fsvSTD = fsView.getSystemTypeDescription(root);
}
catch (Exception e) {
//Windows expects the A drive to exist; if it does not exist, an exception results. Ignore it.
}
return String.format("%s (%s)", fsvSTD, formatRootPathForDisplay(root));
}
/**
* Return a description string for a root location.
* <p>
* Called from a {@link FileDescriptionThread background thread} to avoid blocking the UI
* while waiting for slow file systems.
* <p>
* @param root
* @return string such as "Local Disk (C:)", "Network Drive (R:)"
*/
protected String getRootDescriptionString(File root) {
// Special case the description of the root of a unix filesystem, otherwise it gets marked as removable
if ("/".equals(root.getAbsolutePath())) {
return "File system root (/)";
}
// Special case the description of floppies and removable disks, otherwise delegate to fsView's getSystemDisplayName.
if (fsView.isFloppyDrive(root)) {
return String.format("Floppy (%s)", root.getAbsolutePath());
}
String fsvSTD = null;
try {
fsvSTD = fsView.getSystemTypeDescription(root);
}
catch (Exception e) {
//Windows expects the A drive to exist; if it does not exist, an exception results. Ignore it
}
if (fsvSTD == null || fsvSTD.toLowerCase().indexOf("removable") != -1) {
return "Removable Disk (" + root.getAbsolutePath() + ")";
}
// call the (possibly slow) fsv's getSystemDisplayName
return fsView.getSystemDisplayName(root);
}
/**
* Returns the string path of a file system root, formatted so it doesn't have a trailing backslash in the case
* of Windows root drive strings such as "c:\\", which becomes "c:"
*/
protected String formatRootPathForDisplay(File root) {
String s = root.getAbsolutePath();
return s.length() > 1 && s.endsWith("\\") ? s.substring(0, s.length() - 1) : s;
}
/**
* @see ghidra.util.filechooser.GhidraFileChooserModel#getListing(java.io.File, java.io.FileFilter)
*/
@Override
public File[] getListing(File directory, final FileFilter filter) {
public List<File> getListing(File directory, FileFilter filter) {
// This clears the previously cached icons and avoids issues with modifying the map
// while its being used by other methods by throwing away the instance and allocating
// a new one.
fileIconMap = new HashMap<>();
if (directory == null) {
return new File[0];
return List.of();
}
File[] files = directory.listFiles(filter);
return (files == null) ? new File[0] : files;
return (files == null) ? List.of() : List.of(files);
}
/**
* @see ghidra.util.filechooser.GhidraFileChooserModel#getIcon(java.io.File)
*/
@Override
public Icon getIcon(File file) {
Icon result = rootIconMap.get(file);
if (result == null && file != null && file.exists()) {
result = fileIconMap.computeIfAbsent(file, this::getSystemIcon);
if (FS_ROOT_INFO.isRoot(file)) {
return FS_ROOT_INFO.getRootIcon(file);
}
Icon result = (file != null && file.exists())
? fileIconMap.computeIfAbsent(file, this::getSystemIcon)
: null;
return (result != null) ? result : PROBLEM_FILE_ICON;
}
private Icon getSystemIcon(File file) {
try {
return fsView.getSystemIcon(file);
return FS_VIEW.getSystemIcon(file);
}
catch (Exception e) {
// ignore, return null
@ -218,45 +125,25 @@ public class LocalFileChooserModel implements GhidraFileChooserModel {
return null;
}
/**
* @see ghidra.util.filechooser.GhidraFileChooserModel#getDescription(java.io.File)
*/
@Override
public String getDescription(File file) {
synchronized (rootDescripMap) {
if (rootDescripMap.containsKey(file)) {
return rootDescripMap.get(file);
}
if (FS_ROOT_INFO.isRoot(file)) {
return FS_ROOT_INFO.getRootDescriptionString(file);
}
return fsView.getSystemTypeDescription(file);
return FS_VIEW.getSystemTypeDescription(file);
}
/**
* @see ghidra.util.filechooser.GhidraFileChooserModel#createDirectory(java.io.File, java.lang.String)
*/
@Override
public boolean createDirectory(File directory, String name) {
File newDir = new File(directory, name);
return newDir.mkdir();
}
/**
* @see ghidra.util.filechooser.GhidraFileChooserModel#isDirectory(java.io.File)
*/
@Override
public boolean isDirectory(File file) {
File[] localRoots = getRoots(false);
for (int i = 0; i < localRoots.length; i++) {
if (localRoots[i].equals(file)) {
return true;
}
}
return file != null && file.isDirectory();
return file != null && (FS_ROOT_INFO.isRoot(file) || file.isDirectory());
}
/**
* @see ghidra.util.filechooser.GhidraFileChooserModel#isAbsolute(java.io.File)
*/
@Override
public boolean isAbsolute(File file) {
if (file != null) {
@ -265,36 +152,195 @@ public class LocalFileChooserModel implements GhidraFileChooserModel {
return false;
}
/**
* @see ghidra.util.filechooser.GhidraFileChooserModel#renameFile(java.io.File, java.io.File)
*/
@Override
public boolean renameFile(File src, File dest) {
for (File root : roots) {
if (root.equals(src)) {
return false;
}
if (FS_ROOT_INFO.isRoot(src)) {
return false;
}
return src.renameTo(dest);
}
private class FileDescriptionThread extends Thread {
//---------------------------------------------------------------------------------------------
FileDescriptionThread() {
super("File Chooser - File Description Thread");
/**
* Handles querying / caching information about file system root locations.
* <p>
* Only a single instance of this class is needed and can be shared statically.
*/
private static class FileSystemRootInfo {
private Map<File, String> descriptionMap = new ConcurrentHashMap<>();
private Map<File, Icon> iconMap = new ConcurrentHashMap<>();
private List<File> roots = List.of();
private AtomicBoolean updatePending = new AtomicBoolean();
synchronized boolean isEmpty() {
return roots.isEmpty();
}
@Override
public void run() {
synchronized (rootDescripMap) {
for (File r : roots) {
rootDescripMap.put(r, getRootDescriptionString(r));
synchronized boolean isRoot(File f) {
for (File root : roots) {
if (root.equals(f)) {
return true;
}
}
if (listener != null) {
listener.modelChanged();
return false;
}
/**
* Returns the currently known root locations.
*
* @return list of currently known root locations
*/
synchronized List<File> getRoots() {
return new ArrayList<>(roots);
}
Icon getRootIcon(File root) {
return iconMap.get(root);
}
String getRootDescriptionString(File root) {
return descriptionMap.get(root);
}
/**
* If there is no pending update, updates information about the root filesystem locations
* present on the local computer, in a partially blocking manner. The initial list
* of locations is queried directly, and the descriptions and icons for the root
* locations are fetched in a background thread.
* <p>
* When new information is found during the background querying, the listener callback
* will be executed so that it can cause UI updates.
* <p>
* If there is a pending background update, no-op.
*
* @param callback callback
*/
void updateRootInfo(Callback callback) {
if (updatePending.compareAndSet(false, true)) {
File[] localRoots = listRoots(); // possibly sloooow
synchronized (this) {
roots = List.of(localRoots);
}
for (File root : localRoots) {
descriptionMap.put(root, getInitialRootDescriptionString(root));
iconMap.put(root, PENDING_ROOT_ICON);
}
Thread updateThread = new Thread(
() -> asyncUpdateRootInfo(localRoots, Callback.dummyIfNull(callback)));
updateThread.setName("GhidraFileChooser File System Updater");
updateThread.start();
// updateThread will unset the updatePending flag when done
}
}
private File[] listRoots() {
File[] tmpRoots = File.listRoots(); // possibly sloooow
// File.listRoots javadoc says null result possible (but actual jdk code doesn't do it)
return tmpRoots != null ? tmpRoots : new File[0];
}
private void asyncUpdateRootInfo(File[] localRoots, Callback callback) {
try {
// Populate root description strings with values that are hopefully faster to
// get than the full description strings that will be fetched next.
for (File root : localRoots) {
String fastRootDescriptionString = getFastRootDescriptionString(root);
if (fastRootDescriptionString != null) {
descriptionMap.put(root, fastRootDescriptionString);
callback.call();
}
}
// Populate root description strings with final values, and icons
for (File root : localRoots) {
String slowRootDescriptionString = getSlowRootDescriptionString(root);
if (slowRootDescriptionString != null) {
descriptionMap.put(root, slowRootDescriptionString);
callback.call();
}
Icon rootIcon = FS_VIEW.getSystemIcon(root); // possibly a slow call
iconMap.put(root, rootIcon);
callback.call();
}
}
finally {
updatePending.set(false);
}
}
private String getInitialRootDescriptionString(File root) {
return String.format("Unknown (%s)", formatRootPathForDisplay(root));
}
/**
* Return a description string for a file system root. Avoid slow calls (such as
* {@link FileSystemView#getSystemDisplayName(File)}.
* <p>
* @param root file location
* @return formatted description string, example "Local Drive (C:)"
*/
private String getFastRootDescriptionString(File root) {
try {
String fsvSTD = FS_VIEW.getSystemTypeDescription(root);
return String.format("%s (%s)", fsvSTD, formatRootPathForDisplay(root));
}
catch (Exception e) {
//Windows expects the A drive to exist; if it does not exist, an exception results.
//Ignore it.
}
return null;
}
/**
* Returns the string path of a file system root, formatted so it doesn't have a trailing
* backslash in the case of Windows root drive strings such as "c:\\", which becomes "c:"
*
* @param root file location
* @return string path, formatted to not contain unneeded trailing slashes, example "C:"
* instead of "C:\\"
*/
private String formatRootPathForDisplay(File root) {
String s = root.getPath();
return s.length() > 1 && s.endsWith("\\") ? s.substring(0, s.length() - 1) : s;
}
/**
* Return a description string for a root location.
* <p>
* @param root location to get description string
* @return string such as "Local Disk (C:)", "Network Drive (R:)"
*/
private String getSlowRootDescriptionString(File root) {
// Special case the description of the root of a unix filesystem, otherwise it gets
// marked as removable
if ("/".equals(root.getPath())) {
return "File system root (/)";
}
// Special case the description of floppies and removable disks, otherwise delegate to
// fsView's getSystemDisplayName.
if (FS_VIEW.isFloppyDrive(root)) {
return String.format("Floppy (%s)", formatRootPathForDisplay(root));
}
String fsvSTD = null;
try {
fsvSTD = FS_VIEW.getSystemTypeDescription(root);
}
catch (Exception e) {
//Windows expects the A drive to exist; if it does not exist, an exception results.
//Ignore it
}
if (fsvSTD == null || fsvSTD.toLowerCase().indexOf("removable") != -1) {
return String.format("Removable Disk (%s)", formatRootPathForDisplay(root));
}
// call the (possibly slow) fsv's getSystemDisplayName
return FS_VIEW.getSystemDisplayName(root);
}
}
}

View File

@ -1338,8 +1338,8 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
DirectoryList dirlist = getListView();
DirectoryListModel listModel = (DirectoryListModel) dirlist.getModel();
File[] roots = chooser.getModel().getRoots(false);
assertEquals(roots.length, listModel.getSize());
List<File> roots = chooser.getModel().getRoots(false);
assertEquals(roots.size(), listModel.getSize());
for (File element : roots) {
listModel.contains(element);
}
@ -1450,8 +1450,8 @@ public class GhidraFileChooserTest extends AbstractDockingTest {
// check the chooser contents
DirectoryList dirlist = getListView();
DirectoryListModel listModel = (DirectoryListModel) dirlist.getModel();
File[] listing = chooser.getModel().getListing(homeDir, null);
assertEquals(listing.length, listModel.getSize());
List<File> listing = chooser.getModel().getListing(homeDir, null);
assertEquals(listing.size(), listModel.getSize());
for (File element : listing) {
listModel.contains(element);
}

View File

@ -1,30 +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.util.filechooser;
/**
* A listener for notifying when the contents
* of the file chooser model have changed.
*
*/
public interface GhidraFileChooserListener {
/**
* Invoked when the contents of the file
* chooser model have changed.
*/
public void modelChanged();
}

View File

@ -15,11 +15,15 @@
*/
package ghidra.util.filechooser;
import java.util.List;
import java.io.File;
import java.io.FileFilter;
import javax.swing.Icon;
import utility.function.Callback;
/**
* Interface for the GhidraFileChooser data model.
* This allows the GhidraFileChooser to operate
@ -28,13 +32,15 @@ import javax.swing.Icon;
*/
public interface GhidraFileChooserModel {
/**
* Set the model listener.
* @param l the new model listener
* Set the model update callback.
*
* @param callback the new model update callback handler
*/
public void setListener(GhidraFileChooserListener l);
public void setModelUpdateCallback(Callback callback);
/**
* Returns the home directory.
*
* @return the home directory
*/
public File getHomeDirectory();
@ -43,12 +49,13 @@ public interface GhidraFileChooserModel {
* Returns the user's desktop directory, as defined by their operating system and/or their windowing environment, or
* null if there is no desktop directory.<p>
* Example: "/home/the_user/Desktop" or "c:/Users/the_user/Desktop"
*
* @return desktop directory
*/
public File getDesktopDirectory();
/**
* Returns the root drives/directories.
* Returns a list of the root drives/directories.
* <p>
* On windows, "C:\", "D:\", etc.
* <p>
@ -57,18 +64,20 @@ public interface GhidraFileChooserModel {
* @param forceUpdate if true, request a fresh listing, if false allow a cached result
* @return the root drives
*/
public File[] getRoots(boolean forceUpdate);
public List<File> getRoots(boolean forceUpdate);
/**
* Returns an array of the files that
* exist in the specified directory.
*
* @param directory the directory
* @return an array of files
* @return list of files
*/
public File[] getListing(File directory, FileFilter filter);
public List<File> getListing(File directory, FileFilter filter);
/**
* Returns an icon for the specified file.
*
* @param file the file
* @return an icon for the specified file
*/