mirror of
https://github.com/NationalSecurityAgency/ghidra.git
synced 2025-01-31 21:41:07 +00:00
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:
parent
b6501c8283
commit
c99f770b23
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
@ -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
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user