* progress dialog for move/rename job

This commit is contained in:
Reinhard Pointner 2011-11-04 07:45:48 +00:00
parent 5184e4d98d
commit 47ac797ec3
4 changed files with 196 additions and 60 deletions

View File

@ -82,7 +82,7 @@ public class NotificationLogging extends Handler {
protected String getMessage(LogRecord record) {
String message = record.getMessage();
if (message == null || message.isEmpty()) {
if ((message == null || message.isEmpty()) && record.getThrown() != null) {
// if message is empty, display exception string
return ExceptionUtilities.getMessage(record.getThrown());
}

View File

@ -7,22 +7,36 @@ import static net.sourceforge.filebot.ui.NotificationLogging.*;
import static net.sourceforge.tuned.FileUtilities.*;
import static net.sourceforge.tuned.ui.TunedUtilities.*;
import java.awt.Cursor;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeEvent;
import java.io.File;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import java.util.AbstractMap.SimpleEntry;
import java.util.Map.Entry;
import java.util.concurrent.CancellationException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import javax.swing.AbstractAction;
import javax.swing.Icon;
import javax.swing.SwingWorker;
import net.sourceforge.filebot.Analytics;
import net.sourceforge.filebot.ResourceManager;
import net.sourceforge.tuned.ui.ProgressDialog;
import net.sourceforge.tuned.ui.SwingWorkerPropertyChangeAdapter;
import net.sourceforge.tuned.ui.ProgressDialog.Cancellable;
class RenameAction extends AbstractAction {
@ -40,69 +54,71 @@ class RenameAction extends AbstractAction {
public void actionPerformed(ActionEvent evt) {
List<Entry<File, File>> renameLog = new ArrayList<Entry<File, File>>();
if (model.getRenameMap().isEmpty()) {
return;
}
try {
for (Entry<File, File> mapping : validate(model.getRenameMap(), getWindow(evt.getSource()))) {
// rename file, throw exception on failure
renameFile(mapping.getKey(), mapping.getValue());
// remember successfully renamed matches for history entry and possible revert
renameLog.add(mapping);
}
Window window = getWindow(evt.getSource());
Map<File, File> renameMap = checkRenamePlan(validate(model.getRenameMap(), window));
// renamed all matches successfully
if (renameLog.size() > 0) {
UILogger.info(String.format("%d files renamed.", renameLog.size()));
window.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
RenameJob renameJob = new RenameJob(renameMap);
renameJob.execute();
try {
// wait a for little while (renaming might finish in less than a second)
renameJob.get(2, TimeUnit.SECONDS);
} catch (TimeoutException ex) {
// move/renaming will probably take a while
ProgressDialog dialog = createProgressDialog(window, renameJob);
dialog.setLocation(getOffsetLocation(dialog.getOwner()));
// display progress dialog and stop blocking EDT
window.setCursor(Cursor.getDefaultCursor());
dialog.setVisible(true);
}
} catch (Exception e) {
// could not rename one of the files, revert all changes
UILogger.warning(e.getMessage());
// revert rename operations in reverse order
for (ListIterator<Entry<File, File>> iterator = renameLog.listIterator(renameLog.size()); iterator.hasPrevious();) {
Entry<File, File> mapping = iterator.previous();
// revert rename
File original = mapping.getKey();
File current = new File(original.getParentFile(), mapping.getValue().getPath());
if (current.renameTo(original)) {
// remove reverted rename operation from log
iterator.remove();
} else {
// failed to revert rename operation
UILogger.severe("Failed to revert file: " + mapping.getValue());
}
}
}
// collect renamed types
List<Class> types = new ArrayList<Class>();
// remove renamed matches
for (Entry<File, ?> entry : renameLog) {
// find index of source file
int index = model.files().indexOf(entry.getKey());
types.add(model.values().get(index).getClass());
// remove complete match
model.matches().remove(index);
}
// update history
if (renameLog.size() > 0) {
HistorySpooler.getInstance().append(renameLog);
for (Class it : new HashSet<Class>(types)) {
Analytics.trackEvent("GUI", "Rename", it.getSimpleName(), frequency(types, it));
}
}
}
private Iterable<Entry<File, File>> validate(Map<File, String> renameMap, Window parent) {
private Map<File, File> checkRenamePlan(List<Entry<File, File>> renamePlan) {
// build rename map and perform some sanity checks
Map<File, File> renameMap = new HashMap<File, File>();
Set<File> destinationSet = new TreeSet<File>();
for (Entry<File, File> mapping : renamePlan) {
File source = mapping.getKey();
File destination = mapping.getValue();
// resolve destination
if (!destination.isAbsolute()) {
// same folder, different name
destination = new File(source.getParentFile(), destination.getPath());
}
if (renameMap.containsKey(source))
throw new IllegalArgumentException("Duplicate source file: " + source);
if (destinationSet.contains(destination))
throw new IllegalArgumentException("Conflict detected: " + destination);
if (destination.exists() && !source.equals(destination))
throw new IllegalArgumentException("File already exists: " + destination);
renameMap.put(mapping.getKey(), mapping.getValue());
}
return renameMap;
}
private List<Entry<File, File>> validate(Map<File, String> renameMap, Window parent) {
final List<Entry<File, File>> source = new ArrayList<Entry<File, File>>(renameMap.size());
for (Entry<File, String> entry : renameMap.entrySet()) {
source.add(new SimpleEntry<File, File>(entry.getKey(), new File(entry.getValue())));
}
@ -135,4 +151,113 @@ class RenameAction extends AbstractAction {
// return empty list if validation was cancelled
return emptyList();
}
protected ProgressDialog createProgressDialog(Window parent, final RenameJob job) {
final ProgressDialog dialog = new ProgressDialog(parent, job);
// configure dialog
dialog.setTitle("Renaming...");
dialog.setTitle("Moving 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.setProgress(i, n);
dialog.setNote(String.format("%d of %d", i + 1, n));
}
}
@Override
protected void done(PropertyChangeEvent evt) {
dialog.close();
}
});
return dialog;
}
protected class RenameJob extends SwingWorker<Map<File, File>, Void> implements Cancellable {
private final Map<File, File> renameMap;
private final Map<File, File> renameLog;
public RenameJob(Map<File, File> renameMap) {
this.renameMap = synchronizedMap(renameMap);
this.renameLog = synchronizedMap(new LinkedHashMap<File, File>());
}
@Override
protected Map<File, File> doInBackground() throws Exception {
for (Entry<File, File> mapping : renameMap.entrySet()) {
if (isCancelled())
return renameLog;
// update progress dialog
firePropertyChange("currentFile", mapping.getKey(), mapping.getValue());
// rename file, throw exception on failure
renameFile(mapping.getKey(), mapping.getValue());
// remember successfully renamed matches for history entry and possible revert
renameLog.put(mapping.getKey(), mapping.getValue());
}
return renameLog;
}
@Override
protected void done() {
try {
get(); // check exceptions
} catch (CancellationException e) {
// ignore
} catch (Exception e) {
UILogger.log(Level.SEVERE, e.getMessage(), e);
}
// collect renamed types
List<Class> types = new ArrayList<Class>();
// remove renamed matches
for (File source : renameLog.keySet()) {
// find index of source file
int index = model.files().indexOf(source);
types.add(model.values().get(index).getClass());
// remove complete match
model.matches().remove(index);
}
if (renameLog.size() > 0) {
UILogger.info(String.format("%d files renamed.", renameLog.size()));
HistorySpooler.getInstance().append(renameLog.entrySet());
// count global statistics
for (Class it : new HashSet<Class>(types)) {
Analytics.trackEvent("GUI", "Rename", it.getSimpleName(), frequency(types, it));
}
}
}
@Override
public boolean cancel() {
return cancel(true);
}
}
}

View File

@ -56,6 +56,14 @@ public class ProgressDialog extends JDialog {
}
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);
}
@ -68,11 +76,6 @@ public class ProgressDialog extends JDialog {
}
public JProgressBar getProgressBar() {
return progressBar;
}
public void close() {
setVisible(false);
dispose();
@ -83,6 +86,7 @@ public class ProgressDialog extends JDialog {
@Override
public void actionPerformed(ActionEvent e) {
cancelAction.setEnabled(false);
cancellable.cancel();
}
};

View File

@ -11,10 +11,13 @@ import javax.swing.SwingWorker.StateValue;
public abstract class SwingWorkerPropertyChangeAdapter implements PropertyChangeListener {
public void propertyChange(PropertyChangeEvent evt) {
if (evt.getPropertyName().equals("progress"))
if (evt.getPropertyName().equals("progress")) {
progress(evt);
else if (evt.getPropertyName().equals("state"))
} else if (evt.getPropertyName().equals("state")) {
state(evt);
} else {
event(evt.getPropertyName(), evt.getOldValue(), evt.getNewValue());
}
}
@ -41,4 +44,8 @@ public abstract class SwingWorkerPropertyChangeAdapter implements PropertyChange
protected void done(PropertyChangeEvent evt) {
}
protected void event(String name, Object oldValue, Object newValue) {
}
}