Refactor ProgressDialog

This commit is contained in:
Reinhard Pointner 2016-05-08 01:55:48 +08:00
parent ac5e77ed4e
commit c7b97825f3
8 changed files with 172 additions and 267 deletions

View File

@ -33,5 +33,6 @@
<classpathentry kind="lib" path="lib/ivy/bundle/guava.jar"/>
<classpathentry kind="lib" path="lib/ivy/jar/streamex.jar"/>
<classpathentry kind="lib" path="lib/jars/AppleJavaExtensions.jar"/>
<classpathentry kind="lib" path="lib/ivy/jar/controlsfx.jar" sourcepath="lib/ivy/source/controlsfx.jar"/>
<classpathentry kind="output" path="bin"/>
</classpath>

View File

@ -24,6 +24,7 @@
<dependency org="net.sf.sevenzipjbinding" name="sevenzipjbinding-all-platforms" rev="9.+" />
<dependency org="com.optimaize.languagedetector" name="language-detector" rev="0.+" />
<dependency org="one.util" name="streamex" rev="0.+" />
<dependency org="org.controlsfx" name="controlsfx" rev="8.+" />
<!-- FileBot Scripting -->
<dependency org="org.apache.ant" name="ant" rev="1.9.+" />

View File

@ -52,7 +52,7 @@ public enum NativeRenameAction implements RenameAction {
Shell32.INSTANCE.SHFileOperation(op);
if (op.fAnyOperationsAborted) {
throw new CancellationException();
throw new CancellationException(action.name() + " cancelled");
}
}

View File

@ -14,9 +14,7 @@ import javafx.animation.Interpolator;
import javafx.animation.KeyFrame;
import javafx.animation.KeyValue;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.concurrent.Worker;
import javafx.embed.swing.JFXPanel;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Alert.AlertType;
@ -34,13 +32,7 @@ import net.filebot.ResourceManager;
public class GettingStartedStage {
public static void start() {
// initialize JavaFX
new JFXPanel();
// initialize and show webview
Platform.setImplicitExit(false);
Platform.runLater(() -> {
invokeJavaFX(() -> {
// libjfxwebkit.dylib cannot be deployed on the MAS due to deprecated dependencies
if (isMacSandbox()) {
ask();

View File

@ -11,11 +11,9 @@ import static net.filebot.util.ExceptionUtilities.*;
import static net.filebot.util.FileUtilities.*;
import static net.filebot.util.ui.SwingUI.*;
import java.awt.Dialog.ModalityType;
import java.awt.Dimension;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeEvent;
import java.io.File;
import java.io.IOException;
import java.util.AbstractList;
@ -32,19 +30,17 @@ import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import javax.swing.AbstractAction;
import javax.swing.Icon;
import javax.swing.JList;
import javax.swing.JOptionPane;
import javax.swing.JScrollPane;
import javax.swing.SwingWorker;
import net.filebot.HistorySpooler;
import net.filebot.NativeRenameAction;
@ -52,9 +48,8 @@ import net.filebot.ResourceManager;
import net.filebot.StandardRenameAction;
import net.filebot.mac.MacAppUtilities;
import net.filebot.similarity.Match;
import net.filebot.util.ui.ProgressDialog;
import net.filebot.util.ui.ProgressDialog.Cancellable;
import net.filebot.util.ui.SwingWorkerPropertyChangeAdapter;
import net.filebot.util.ui.ProgressMonitor;
import net.filebot.util.ui.ProgressMonitor.ProgressWorker;
class RenameAction extends AbstractAction {
@ -88,43 +83,48 @@ class RenameAction extends AbstractAction {
return;
}
// start processing
List<Match<Object, File>> matches = new ArrayList<Match<Object, File>>(model.matches());
StandardRenameAction action = (StandardRenameAction) getValue(RENAME_ACTION);
if (useNativeShell() && isNativeActionSupported(action)) {
RenameJob renameJob = new NativeRenameJob(renameMap, NativeRenameAction.valueOf(action.name()));
renameJob.execute();
// start processing
Map<File, File> renameLog = new LinkedHashMap<File, File>();
// wait for native operation to finish or be cancelled
try {
renameJob.get();
} catch (CancellationException e) {
debug.finest(e::toString);
}
} else {
RenameJob renameJob = new RenameJob(renameMap, action);
renameJob.execute();
// wait a little while (renaming might finish in less than a second)
try {
renameJob.get(2, TimeUnit.SECONDS);
} catch (TimeoutException e) {
// display progress dialog because move/rename might take a while
ProgressDialog dialog = createProgressDialog(window, renameJob);
dialog.setModalityType(ModalityType.APPLICATION_MODAL);
dialog.setLocation(getOffsetLocation(dialog.getOwner()));
dialog.setIndeterminate(true);
dialog.setVisible(true);
try {
if (useNativeShell() && isNativeActionSupported(action)) {
// call on EDT
RenameWorker worker = new NativeRenameWorker(renameMap, renameLog, NativeRenameAction.valueOf(action.name()));
worker.call(null, null, null);
} else {
// call and wait
RenameWorker worker = new RenameWorker(renameMap, renameLog, action);
ProgressMonitor.runTask(action.getDisplayName(), String.format("%s %d %s. This may take a while.", action.getDisplayName() + "ing", renameMap.size(), renameMap.size() == 1 ? "file" : "files"), worker).get();
}
} catch (CancellationException e) {
debug.finest(e::toString);
} catch (Exception e) {
log.log(Level.SEVERE, String.format("%s: %s", getRootCause(e).getClass().getSimpleName(), getRootCauseMessage(e)), e);
}
// abort if nothing happened
if (renameLog.isEmpty()) {
return;
}
log.info(String.format("%d files renamed.", renameLog.size()));
// remove renamed matches
renameLog.forEach((from, to) -> {
model.matches().remove(model.files().indexOf(from));
});
HistorySpooler.getInstance().append(renameLog.entrySet());
// store xattr
storeMetaInfo(renameMap, matches);
// delete empty folders
if (action == StandardRenameAction.MOVE) {
deleteEmptyFolders(renameMap);
deleteEmptyFolders(renameLog);
}
});
} catch (ExecutionException e) {
@ -187,12 +187,14 @@ class RenameAction extends AbstractAction {
}
protected void moveToTrash(Collection<File> files) {
// use com.apple.eio package on OS X platform
if (isMacApp()) {
files.stream().forEach(MacAppUtilities::moveToTrash);
return;
}
try {
if (isMacApp()) {
files.stream().forEach(MacAppUtilities::moveToTrash);
} else {
com.sun.jna.platform.FileUtils.getInstance().moveToTrash(files.toArray(new File[0]));
}
com.sun.jna.platform.FileUtils.getInstance().moveToTrash(files.toArray(new File[0]));
} catch (Throwable e) {
debug.log(Level.WARNING, e, e::getMessage);
}
@ -303,164 +305,86 @@ class RenameAction extends AbstractAction {
return emptyList();
}
protected ProgressDialog createProgressDialog(Window parent, final RenameJob job) {
final ProgressDialog dialog = new ProgressDialog(parent, job);
// configure dialog
dialog.setTitle("Processing files...");
dialog.setIcon((Icon) getValue(SMALL_ICON));
// close progress dialog when worker is finished
job.addPropertyChangeListener(new SwingWorkerPropertyChangeAdapter() {
@Override
protected void event(String name, Object oldValue, Object newValue) {
if (name.equals("currentFile")) {
int i = job.renameLog.size();
int n = job.renameMap.size();
dialog.setNote(String.format("%d of %d", i + 1, n));
// OSX LaF progress bar may not display progress bar text in indeterminate mode
if (isMacApp()) {
dialog.setTitle("Processing files... " + String.format("%d of %d", i + 1, n));
}
if (newValue instanceof File) {
dialog.setWindowTitle("Processing " + ((File) oldValue).getName());
}
}
}
@Override
protected void done(PropertyChangeEvent evt) {
dialog.close();
}
});
return dialog;
}
protected class RenameJob extends SwingWorker<Map<File, File>, Void> implements Cancellable {
protected final net.filebot.RenameAction action;
protected static class RenameWorker implements ProgressWorker<Map<File, File>> {
protected final Map<File, File> renameMap;
protected final Map<File, File> renameLog;
protected final Semaphore postprocess = new Semaphore(0);
protected final net.filebot.RenameAction action;
public RenameJob(Map<File, File> renameMap, net.filebot.RenameAction action) {
protected boolean cancelled = false;
public RenameWorker(Map<File, File> renameMap, Map<File, File> renameLog, net.filebot.RenameAction action) {
this.renameMap = renameMap;
this.renameLog = renameLog;
this.action = action;
this.renameMap = synchronizedMap(renameMap);
this.renameLog = synchronizedMap(new LinkedHashMap<File, File>());
}
@Override
protected Map<File, File> doInBackground() throws Exception {
try {
for (Entry<File, File> mapping : renameMap.entrySet()) {
if (isCancelled())
return renameLog;
public Map<File, File> call(Consumer<String> message, BiConsumer<Long, Long> progress, Supplier<Boolean> cancelled) throws Exception {
long i = 0;
long n = renameMap.size();
// update progress dialog
firePropertyChange("currentFile", mapping.getKey(), mapping.getValue());
// rename file, throw exception on failure
File source = mapping.getKey();
File destination = resolve(mapping.getKey(), mapping.getValue());
boolean isSameFile = source.equals(destination);
if (!isSameFile || (isSameFile && !source.getName().equals(destination.getName()))) {
action.rename(source, destination);
}
// remember successfully renamed matches for history entry and possible revert
renameLog.put(mapping.getKey(), mapping.getValue());
for (Entry<File, File> mapping : renameMap.entrySet()) {
if (cancelled.get()) {
return renameLog;
}
} finally {
postprocess.release();
progress.accept(i++, n);
message.accept(String.format("[%d / %d] %s", i, n, mapping.getKey().getName()));
// rename file, throw exception on failure
File source = mapping.getKey();
File destination = resolve(mapping.getKey(), mapping.getValue());
if (!source.equals(destination) || (source.equals(destination) && !source.getName().equals(destination.getName()))) {
action.rename(source, destination);
}
// remember successfully renamed matches for history entry and possible revert
renameLog.put(mapping.getKey(), mapping.getValue());
}
return renameLog;
}
@Override
protected void done() {
try {
postprocess.acquire();
this.get(); // grab exception if any
} catch (Exception e) {
if (!isCancelled()) {
log.log(Level.SEVERE, String.format("%s: %s", getRootCause(e).getClass().getSimpleName(), getRootCauseMessage(e)), e);
} else {
debug.log(Level.SEVERE, e.getMessage(), e);
}
}
// remove renamed matches
for (File source : renameLog.keySet()) {
// find index of source file
int index = model.files().indexOf(source);
// remove complete match
model.matches().remove(index);
}
if (renameLog.size() > 0) {
log.info(String.format("%d files renamed.", renameLog.size()));
HistorySpooler.getInstance().append(renameLog.entrySet());
}
}
@Override
public boolean cancel() {
return cancel(true);
}
}
protected class NativeRenameJob extends RenameJob implements Cancellable {
protected static class NativeRenameWorker extends RenameWorker {
public NativeRenameJob(Map<File, File> renameMap, NativeRenameAction action) {
super(renameMap, action);
public NativeRenameWorker(Map<File, File> renameMap, Map<File, File> renameLog, NativeRenameAction action) {
super(renameMap, renameLog, action);
}
@Override
protected Map<File, File> doInBackground() throws Exception {
public Map<File, File> call(Consumer<String> message, BiConsumer<Long, Long> progress, Supplier<Boolean> cancelled) throws Exception {
NativeRenameAction shell = (NativeRenameAction) action;
// prepare delta, ignore files already named as desired
Map<File, File> todo = new LinkedHashMap<File, File>();
Map<File, File> renamePlan = new LinkedHashMap<File, File>();
for (Entry<File, File> mapping : renameMap.entrySet()) {
File source = mapping.getKey();
File destination = resolve(mapping.getKey(), mapping.getValue());
if (!source.equals(destination)) {
todo.put(source, destination);
renamePlan.put(source, destination);
}
}
// call native shell move/copy
try {
shell.rename(todo);
shell.rename(renamePlan);
} catch (CancellationException e) {
// set as cancelled and propagate the exception
super.cancel(false);
throw e;
} finally {
// check status of renamed files
for (Entry<File, File> it : renameMap.entrySet()) {
if (resolve(it.getKey(), it.getValue()).exists()) {
renameLog.put(it.getKey(), it.getValue());
}
debug.finest(e::getMessage);
}
for (Entry<File, File> it : renameMap.entrySet()) {
if (resolve(it.getKey(), it.getValue()).exists()) {
renameLog.put(it.getKey(), it.getValue());
}
postprocess.release();
}
return renameLog;
}
@Override
public boolean cancel() {
throw new UnsupportedOperationException();
}
}
}

View File

@ -1,99 +0,0 @@
package net.filebot.util.ui;
import static net.filebot.util.ui.SwingUI.*;
import java.awt.Cursor;
import java.awt.Window;
import java.awt.event.ActionEvent;
import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import net.miginfocom.swing.MigLayout;
public class ProgressDialog extends JDialog {
private final JProgressBar progressBar = new JProgressBar(0, 100);
private final JLabel iconLabel = new JLabel();
private final JLabel headerLabel = new JLabel();
private final Cancellable cancellable;
public ProgressDialog(Window owner, Cancellable cancellable) {
super(owner, ModalityType.DOCUMENT_MODAL);
this.cancellable = cancellable;
// disable window close button
setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
headerLabel.setFont(headerLabel.getFont().deriveFont(18f));
progressBar.setIndeterminate(true);
progressBar.setStringPainted(true);
JPanel c = (JPanel) getContentPane();
c.setLayout(new MigLayout("insets dialog, nogrid, fill"));
c.add(iconLabel, "h pref!, w pref!");
c.add(headerLabel, "gap 3mm, wrap paragraph");
c.add(progressBar, "hmin 25px, grow, wrap paragraph");
c.add(new JButton(cancel), "align center");
setSize(350, 155);
}
public void setIcon(Icon icon) {
iconLabel.setIcon(icon);
}
public void setIndeterminate(boolean b) {
progressBar.setIndeterminate(b);
}
public void setProgress(int value, int max) {
progressBar.setIndeterminate(false);
progressBar.setMinimum(0);
progressBar.setValue(value);
progressBar.setMaximum(max);
}
public void setNote(String text) {
progressBar.setString(text);
}
@Override
public void setTitle(String text) {
super.setTitle(text);
headerLabel.setText(text);
}
public void setWindowTitle(String text) {
super.setTitle(text);
}
public void close() {
setVisible(false);
dispose();
}
public void cancel(ActionEvent evt) {
cancel.setEnabled(false);
cancellable.cancel();
}
protected final Action cancel = newAction("Cancel", this::cancel);
public static interface Cancellable {
boolean isCancelled();
boolean cancel();
}
}

View File

@ -0,0 +1,65 @@
package net.filebot.util.ui;
import static net.filebot.Logging.*;
import static net.filebot.util.ui.SwingUI.*;
import java.util.concurrent.FutureTask;
import java.util.function.BiConsumer;
import java.util.function.Consumer;
import java.util.function.Supplier;
import java.util.logging.Level;
import org.controlsfx.dialog.ProgressDialog;
import javafx.concurrent.Task;
import javafx.event.EventHandler;
import javafx.stage.Modality;
import javafx.stage.Stage;
import javafx.stage.WindowEvent;
public class ProgressMonitor<T> {
public static <T> FutureTask<T> runTask(String title, String header, ProgressWorker<T> worker) {
initJavaFX();
Task<T> task = new Task<T>() {
@Override
protected T call() throws Exception {
return worker.call(this::updateMessage, this::updateProgress, this::isCancelled);
}
};
// show progress monitor if operation takes more than 2 seconds
invokeJavaFX(() -> {
try {
ProgressDialog dialog = new ProgressDialog(task);
dialog.initModality(Modality.APPLICATION_MODAL);
dialog.setTitle(title);
dialog.setHeaderText(header);
Stage stage = (Stage) dialog.getDialogPane().getScene().getWindow();
stage.setAlwaysOnTop(true);
stage.setOnCloseRequest(evt -> task.cancel());
} catch (Exception e) {
debug.log(Level.WARNING, e, e::toString);
}
});
Thread thread = new Thread(task);
thread.setDaemon(true);
thread.start();
return task;
}
@FunctionalInterface
public interface ProgressWorker<T> {
T call(Consumer<String> message, BiConsumer<Long, Long> progress, Supplier<Boolean> cancelled) throws Exception;
}
private ProgressMonitor() {
throw new UnsupportedOperationException();
}
}

View File

@ -48,6 +48,8 @@ import javax.swing.plaf.basic.BasicTableUI;
import javax.swing.text.JTextComponent;
import javax.swing.undo.UndoManager;
import javafx.application.Platform;
import javafx.embed.swing.JFXPanel;
import net.filebot.Settings;
public final class SwingUI {
@ -411,6 +413,25 @@ public final class SwingUI {
}
}
private static boolean initJavaFX = true;
public static void initJavaFX() {
if (initJavaFX) {
initJavaFX = false;
// initialize JavaFX
new JFXPanel();
// disable JavaFX exit
Platform.setImplicitExit(false);
}
}
public static void invokeJavaFX(Runnable r) {
initJavaFX();
Platform.runLater(r);
}
/**
* Dummy constructor to prevent instantiation.
*/