Skip to content

Commit

Permalink
Add async file listing, #176
Browse files Browse the repository at this point in the history
  • Loading branch information
kotcrab committed Jul 3, 2016
1 parent 08deace commit ac03bb9
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 53 deletions.
1 change: 1 addition & 0 deletions ui/CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#### Version: 1.1.4-SNAPSHOT (LibGDX 1.9.3)
- **Added**: `BusyBar`
- **Added**: `Tooltip.Builder#width()`, `Tooltip#setText(String)`, `Tooltip#getContentCell()`
- **Changed**: `FileChooser` directory listing is now performed on separate thread to prevent application hanging when accessing unresponsive drive
- **Fixed**: PopupMenu with single item is now accessible using keyboard

#### Version: 1.1.3 (LibGDX 1.9.3)
Expand Down
86 changes: 74 additions & 12 deletions ui/src/main/java/com/kotcrab/vis/ui/widget/file/FileChooser.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
import com.badlogic.gdx.utils.Align;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Scaling;
import com.badlogic.gdx.utils.Timer;
import com.kotcrab.vis.ui.FocusManager;
import com.kotcrab.vis.ui.Focusable;
import com.kotcrab.vis.ui.Sizes;
Expand Down Expand Up @@ -58,6 +59,9 @@
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

import static com.kotcrab.vis.ui.widget.file.internal.FileChooserText.*;

Expand Down Expand Up @@ -92,6 +96,10 @@ public class FileChooser extends VisWindow implements FileHistoryCallback {
private Array<DriveCheckerListener> driveCheckerListeners = new Array<DriveCheckerListener>();
private FileChooserWinService chooserWinService = FileChooserWinService.getInstance();

private ExecutorService listDirExecutor = Executors.newSingleThreadExecutor(new ServiceThreadFactory("FileChooserListDirThread"));
private Future<?> listDirFuture;
private ShotBusyBarTask showBusyBarTask = new ShotBusyBarTask();

private SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");

public static final int DEFAULT_KEY = -1;
Expand Down Expand Up @@ -129,6 +137,7 @@ public class FileChooser extends VisWindow implements FileHistoryCallback {
private VerticalGroup shortcutsFavoritesPanel;
private ListView<FileHandle> fileListView;
private float maxDateLabelWidth;
private BusyBar fileListBusyBar;

private VisImageButton favoriteFolderButton;
private VisImageButton viewModeButton;
Expand Down Expand Up @@ -292,9 +301,7 @@ public boolean keyTyped (InputEvent event, char character) {
return false;
}
float targetWidth = currentPath.getWidth() + showRecentDirButton.getWidth();
dirsSuggestionPopup.pathFieldKeyTyped(getChooserStage(), targetWidth - 20);
dirsSuggestionPopup.setWidth(targetWidth);
dirsSuggestionPopup.layout();
dirsSuggestionPopup.pathFieldKeyTyped(getChooserStage(), targetWidth);
return false;
}

Expand Down Expand Up @@ -330,9 +337,7 @@ public void keyboardFocusChanged (FocusEvent event, Actor actor, boolean focused
@Override
public void changed (ChangeEvent event, Actor actor) {
float targetWidth = currentPath.getWidth() + showRecentDirButton.getWidth();
dirsSuggestionPopup.showRecentDirectories(getChooserStage(), recentDirectories, targetWidth - 20);
dirsSuggestionPopup.setWidth(targetWidth);
dirsSuggestionPopup.layout();
dirsSuggestionPopup.showRecentDirectories(getChooserStage(), recentDirectories, targetWidth);
}
});

Expand Down Expand Up @@ -437,6 +442,9 @@ private void createCenterContentPanel () {
setupDefaultScrollPane(fileListView.getScrollPane());

VisTable fileScrollPaneTable = new VisTable();
fileListBusyBar = new BusyBar();
fileListBusyBar.setVisible(false);
fileScrollPaneTable.add(fileListBusyBar).space(0).height(PrefHeightIfVisibleValue.INSTANCE).growX().row();
fileScrollPaneTable.add(fileListView.getMainTable()).pad(2).top().expand().fillX();
fileScrollPaneTable.setTouchable(Touchable.enabled);

Expand Down Expand Up @@ -824,16 +832,51 @@ private void rebuildShortcutsFavoritesPanel () {

private void rebuildFileList () {
filesListRebuildScheduled = false;
FileHandle[] selectedFiles = new FileHandle[selectedItems.size];
final FileHandle[] selectedFiles = new FileHandle[selectedItems.size];
for (int i = 0; i < selectedFiles.length; i++) {
selectedFiles[i] = selectedItems.get(i).file;
}
deselectAll();

FileHandle[] files = listFilteredCurrentDirectory();
currentPath.setText(currentDirectory.path());

if (showBusyBarTask.isScheduled() == false) {
Timer.schedule(showBusyBarTask, 0.2f); //quite period before busy bar is shown
}

if (listDirFuture != null) listDirFuture.cancel(true);
listDirFuture = listDirExecutor.submit(new Runnable() {
@Override
public void run () {
if (currentDirectory.exists() == false) handleAsyncError("Provided directory does not exist!");
if (currentDirectory.isDirectory() == false)
handleAsyncError("Provided path is a file, not directory!");

final FileHandle[] files = listFilteredCurrentDirectory();
if (Thread.currentThread().isInterrupted()) return;
Gdx.app.postRunnable(new Runnable() {
@Override
public void run () {
buildFileList(files, selectedFiles);
}
});
}
});
}

protected void handleAsyncError (final String cause) {
Gdx.app.postRunnable(new Runnable() {
@Override
public void run () {
throw new IllegalStateException(cause);
}
});
}

private void buildFileList (FileHandle[] files, FileHandle[] selectedFiles) {
currentFiles.clear();
showBusyBarTask.cancel();
fileListBusyBar.setVisible(false);

if (files.length == 0) {
fileListAdapter.itemsChanged();
Expand Down Expand Up @@ -1044,12 +1087,15 @@ public void setDirectory (FileHandle directory) {
}

@Override
/**
* Changes file chooser active directory.
* Warning: To avoid hanging listing directory is performed asynchronously. This implies that this method cannot check
* if directory exist and if provided file handle actually points to directory. Those checks have to be performed in
* separate thread. By default file chooser will post exception to libGDX thread, override {@link #handleAsyncError(String)}
* to change this.
*/
public void setDirectory (FileHandle directory, HistoryPolicy historyPolicy) {
if (directory.equals(currentDirectory)) return;
if (directory.exists() == false) throw new IllegalStateException("Provided directory does not exist!");
if (directory.isDirectory() == false)
throw new IllegalStateException("Provided path is a file, not directory!");

if (historyPolicy == HistoryPolicy.ADD) historyManager.historyAdd();

currentDirectory = directory;
Expand Down Expand Up @@ -1634,6 +1680,22 @@ public boolean delete (FileHandle file) {
}
}

private class ShotBusyBarTask extends Timer.Task {
@Override
public void run () {
fileListBusyBar.resetSegment();
fileListBusyBar.setVisible(true);
currentFiles.clear();
fileListAdapter.itemsChanged();
}

@Override
public synchronized void cancel () {
super.cancel();
fileListBusyBar.setVisible(false);
}
}

/** Internal FileChooser API. */
public class FileItem extends Table implements Focusable {
private FileHandle file;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,24 @@
import com.badlogic.gdx.files.FileHandle;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.actions.Actions;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import com.badlogic.gdx.utils.Array;
import com.kotcrab.vis.ui.widget.MenuItem;
import com.kotcrab.vis.ui.widget.VisTextField;
import com.kotcrab.vis.ui.widget.file.FileChooser;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

/** @author Kotcrab */
public class DirsSuggestionPopup extends AbstractSuggestionPopup {
private final VisTextField pathField;

private ExecutorService listDirExecutor = Executors.newSingleThreadExecutor(new ServiceThreadFactory("FileChooserListDirThread"));
private Future<?> listDirFuture;

public DirsSuggestionPopup (FileChooser chooser, VisTextField pathField) {
super(chooser);
this.pathField = pathField;
Expand All @@ -40,50 +48,73 @@ public void pathFieldKeyTyped (Stage stage, float width) {
remove();
return;
}

int suggestions = createDirSuggestions(width);
if (suggestions == 0) {
remove();
return;
}

showMenu(stage, pathField);
createDirSuggestions(stage, width);
}

private int createDirSuggestions (float width) {
clearChildren();
int suggestions = 0;

FileHandle enteredDir = Gdx.files.absolute(pathField.getText());
String partialPath = "";
if (enteredDir.exists() == false) {
partialPath = enteredDir.name();
enteredDir = enteredDir.parent();
}

for (final FileHandle file : enteredDir.list(chooser.getFileFilter())) {
if (file.exists() == false || file.isDirectory() == false) continue;
if (file.name().startsWith(partialPath) == false || file.name().equals(partialPath)) continue;

MenuItem item = createMenuItem(file.path());
item.getLabel().setEllipsis(true);
item.getLabelCell().width(width);
addItem(item);

item.addListener(new ChangeListener() {
@Override
public void changed (ChangeEvent event, Actor actor) {
chooser.setDirectory(file, FileChooser.HistoryPolicy.ADD);
private void createDirSuggestions (final Stage stage, final float width) {
final String pathFieldText = pathField.getText();
//quiet period before listing files takes too long and popup will be removed
addAction(Actions.sequence(Actions.delay(0.2f, Actions.removeActor())));

if (listDirFuture != null) listDirFuture.cancel(true);
listDirFuture = listDirExecutor.submit(new Runnable() {
@Override
public void run () {
FileHandle enteredDir = Gdx.files.absolute(pathFieldText);
final FileHandle listDir;
final String partialPath;
if (enteredDir.exists()) {
listDir = enteredDir;
partialPath = "";
} else {
listDir = enteredDir.parent();
partialPath = enteredDir.name();
}
});

suggestions++;
if (suggestions == MAX_SUGGESTIONS) {
break;
final FileHandle[] files = listDir.list(chooser.getFileFilter());
if (Thread.currentThread().isInterrupted()) return;
Gdx.app.postRunnable(new Runnable() {
@Override
public void run () {
clearChildren();
clearActions();
int suggestions = 0;

for (final FileHandle file : files) {
if (file.exists() == false || file.isDirectory() == false) continue;
if (file.name().startsWith(partialPath) == false || file.name().equals(partialPath))
continue;

MenuItem item = createMenuItem(file.path());
item.getLabel().setEllipsis(true);
item.getLabelCell().width(width - 20);
addItem(item);

item.addListener(new ChangeListener() {
@Override
public void changed (ChangeEvent event, Actor actor) {
chooser.setDirectory(file, FileChooser.HistoryPolicy.ADD);
}
});

suggestions++;
if (suggestions == MAX_SUGGESTIONS) {
break;
}
}

if (suggestions == 0) {
remove();
return;
}

showMenu(stage, pathField);
setWidth(width);
layout();
}
});
}
}

return suggestions;
});
}

public void showRecentDirectories (Stage stage, Array<FileHandle> recentDirectories, float width) {
Expand All @@ -92,8 +123,9 @@ public void showRecentDirectories (Stage stage, Array<FileHandle> recentDirector
remove();
return;
}

showMenu(stage, pathField);
setWidth(width);
layout();
}

private int createRecentDirSuggestions (Array<FileHandle> files, float width) {
Expand All @@ -104,7 +136,7 @@ private int createRecentDirSuggestions (Array<FileHandle> files, float width) {

MenuItem item = createMenuItem(file.path());
item.getLabel().setEllipsis(true);
item.getLabelCell().width(width);
item.getLabelCell().width(width - 20);
addItem(item);

item.addListener(new ChangeListener() {
Expand All @@ -123,3 +155,4 @@ public void changed (ChangeEvent event, Actor actor) {
return suggestions;
}
}

0 comments on commit ac03bb9

Please sign in to comment.