From c99f770b230dae36eaa50e8a41dce0ee81c360b0 Mon Sep 17 00:00:00 2001 From: dev747368 <48332326+dev747368@users.noreply.github.com> Date: Thu, 2 Jun 2022 14:57:58 -0400 Subject: [PATCH] GP-2059 improve GhidraFileChooser interactivity Refactor how file system root locations are handled to avoid potential slowdowns and swing thread blocking. --- .../plugins/fsbrowser/FSBActionManager.java | 31 +- .../filechooser/DirectoryTableModel.java | 33 +- .../widgets/filechooser/FileComparator.java | 25 +- .../filechooser/GhidraFileChooser.java | 38 +- .../filechooser/LocalFileChooserModel.java | 372 ++++++++++-------- .../filechooser/GhidraFileChooserTest.java | 8 +- .../GhidraFileChooserListener.java | 30 -- .../filechooser/GhidraFileChooserModel.java | 23 +- 8 files changed, 287 insertions(+), 273 deletions(-) delete mode 100644 Ghidra/Framework/Generic/src/main/java/ghidra/util/filechooser/GhidraFileChooserListener.java diff --git a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBActionManager.java b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBActionManager.java index b3e7c5f711..cfd13c0247 100644 --- a/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBActionManager.java +++ b/Ghidra/Features/Base/src/main/java/ghidra/plugins/fsbrowser/FSBActionManager.java @@ -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) { diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/DirectoryTableModel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/DirectoryTableModel.java index b1a87c58b9..4d47c8ff80 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/DirectoryTableModel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/DirectoryTableModel.java @@ -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 { @@ -28,7 +29,7 @@ class DirectoryTableModel extends AbstractSortedTableModel { final static int TIME_COL = 2; private GhidraFileChooser chooser; - private File[] files = new File[0]; + private List files = new ArrayList<>(); DirectoryTableModel(GhidraFileChooser chooser) { super(FILE_COL); @@ -36,31 +37,27 @@ class DirectoryTableModel extends AbstractSortedTableModel { } 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 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 { @Override public int getRowCount() { - return files == null ? 0 : files.length; + return files.size(); } @Override @@ -129,7 +126,7 @@ class DirectoryTableModel extends AbstractSortedTableModel { @Override public List getModelData() { - return Arrays.asList(files); + return files; } @Override @@ -140,7 +137,7 @@ class DirectoryTableModel extends AbstractSortedTableModel { @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 { switch (column) { case FILE_COL: - files[row] = (File) aValue; + files.set(row, (File) aValue); update(); break; } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/FileComparator.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/FileComparator.java index 20f4134d8e..e1c33a316e 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/FileComparator.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/FileComparator.java @@ -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 { final static int SORT_BY_NAME = 1111; @@ -59,7 +59,7 @@ class FileComparator implements Comparator { 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 { 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; - } - } 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 b5306ce825..f38f9d0c3c 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 @@ -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; *
  • This class provides shortcut buttons similar to those of the Windows native chooser
  • * */ -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) 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 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)); } } diff --git a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/LocalFileChooserModel.java b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/LocalFileChooserModel.java index c764a4e56b..7399a6c139 100644 --- a/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/LocalFileChooserModel.java +++ b/Ghidra/Framework/Docking/src/main/java/docking/widgets/filechooser/LocalFileChooserModel.java @@ -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 rootDescripMap = new HashMap<>(); - private Map 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. *

    - * 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 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. - *

    - * Returns null if the desktop directory is missing. - *

    - * @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 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)}. - *

    - * 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. - *

    - * Called from a {@link FileDescriptionThread background thread} to avoid blocking the UI - * while waiting for slow file systems. - *

    - * @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 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. + *

    + * Only a single instance of this class is needed and can be shared statically. + */ + private static class FileSystemRootInfo { + private Map descriptionMap = new ConcurrentHashMap<>(); + private Map iconMap = new ConcurrentHashMap<>(); + private List 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 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. + *

    + * When new information is found during the background querying, the listener callback + * will be executed so that it can cause UI updates. + *

    + * 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)}. + *

    + * @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. + *

    + * @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); + } } } diff --git a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/filechooser/GhidraFileChooserTest.java b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/filechooser/GhidraFileChooserTest.java index ebf5fff4db..8f9b036e65 100644 --- a/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/filechooser/GhidraFileChooserTest.java +++ b/Ghidra/Framework/Docking/src/test.slow/java/docking/widgets/filechooser/GhidraFileChooserTest.java @@ -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 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 listing = chooser.getModel().getListing(homeDir, null); + assertEquals(listing.size(), listModel.getSize()); for (File element : listing) { listModel.contains(element); } diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/filechooser/GhidraFileChooserListener.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/filechooser/GhidraFileChooserListener.java deleted file mode 100644 index f691e2af55..0000000000 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/filechooser/GhidraFileChooserListener.java +++ /dev/null @@ -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(); -} diff --git a/Ghidra/Framework/Generic/src/main/java/ghidra/util/filechooser/GhidraFileChooserModel.java b/Ghidra/Framework/Generic/src/main/java/ghidra/util/filechooser/GhidraFileChooserModel.java index 9a0918a5b8..3d48a621b4 100644 --- a/Ghidra/Framework/Generic/src/main/java/ghidra/util/filechooser/GhidraFileChooserModel.java +++ b/Ghidra/Framework/Generic/src/main/java/ghidra/util/filechooser/GhidraFileChooserModel.java @@ -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.

    * 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. *

    * On windows, "C:\", "D:\", etc. *

    @@ -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 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 getListing(File directory, FileFilter filter); /** * Returns an icon for the specified file. + * * @param file the file * @return an icon for the specified file */