* introduction of a single unified Rename- and MatchModel

* arbitrary formating using Object (e.g. episode information) and File (e.g. codec information) data
* background formatting (e.g. crc32) and visual indicators in cellrenderer
This commit is contained in:
Reinhard Pointner 2009-04-26 13:34:22 +00:00
parent 54bf7c2ca3
commit d5a5b93b3b
28 changed files with 1100 additions and 555 deletions

View File

@ -114,7 +114,7 @@ public class EpisodeFormatBindingBean {
@Define("crc32")
public String getCRC32() throws IOException {
public String getCRC32() throws IOException, InterruptedException {
if (mediaFile != null) {
// try to get checksum from file name
String embeddedChecksum = FileBotUtilities.getEmbeddedChecksum(mediaFile.getName());
@ -171,7 +171,7 @@ public class EpisodeFormatBindingBean {
}
public synchronized MediaInfo getMediaInfo() {
private synchronized MediaInfo getMediaInfo() {
if (mediaFile == null) {
throw new NullPointerException("Media file is null");
}
@ -203,7 +203,7 @@ public class EpisodeFormatBindingBean {
private static final Cache checksumCache = CacheManager.getInstance().getCache("checksum");
private String crc32(File file) throws IOException {
private String crc32(File file) throws IOException, InterruptedException {
// try to get checksum from cache
Element cacheEntry = checksumCache.get(file);
@ -221,6 +221,10 @@ public class EpisodeFormatBindingBean {
while ((len = in.read(buffer)) >= 0) {
crc.update(buffer, 0, len);
// make this long-running operation interruptible
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
in.close();

View File

@ -24,16 +24,16 @@ import com.sun.phobos.script.javascript.RhinoScriptEngine;
public class ExpressionFormat extends Format {
private final String format;
private final String expression;
private final Object[] expressions;
private final Object[] compilation;
private ScriptException lastException;
public ExpressionFormat(String format) throws ScriptException {
this.format = format;
this.expressions = compile(format, (Compilable) initScriptEngine());
public ExpressionFormat(String expression) throws ScriptException {
this.expression = expression;
this.compilation = compile(expression, (Compilable) initScriptEngine());
}
@ -47,40 +47,40 @@ public class ExpressionFormat extends Format {
}
public String getFormat() {
return format;
public String getExpression() {
return expression;
}
protected Object[] compile(String format, Compilable engine) throws ScriptException {
List<Object> expression = new ArrayList<Object>();
protected Object[] compile(String expression, Compilable engine) throws ScriptException {
List<Object> compilation = new ArrayList<Object>();
Matcher matcher = Pattern.compile("\\{([^\\{]*?)\\}").matcher(format);
Matcher matcher = Pattern.compile("\\{([^\\{]*?)\\}").matcher(expression);
int position = 0;
while (matcher.find()) {
if (position < matcher.start()) {
// literal before
expression.add(format.substring(position, matcher.start()));
compilation.add(expression.substring(position, matcher.start()));
}
String script = matcher.group(1);
if (script.length() > 0) {
// compiled script, or literal
expression.add(engine.compile(script));
compilation.add(engine.compile(script));
}
position = matcher.end();
}
if (position < format.length()) {
if (position < expression.length()) {
// tail
expression.add(format.substring(position, format.length()));
compilation.add(expression.substring(position, expression.length()));
}
return expression.toArray();
return compilation.toArray();
}
@ -99,7 +99,7 @@ public class ExpressionFormat extends Format {
ScriptContext context = new SimpleScriptContext();
context.setBindings(bindings, ScriptContext.GLOBAL_SCOPE);
for (Object snipped : expressions) {
for (Object snipped : compilation) {
if (snipped instanceof CompiledScript) {
try {
Object value = ((CompiledScript) snipped).eval(context);

Binary file not shown.

After

Width:  |  Height:  |  Size: 744 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 866 B

View File

@ -2,24 +2,24 @@
package net.sourceforge.filebot.similarity;
public class Match<V, C> {
public class Match<Value, Candidate> {
private final V value;
private final C candidate;
private final Value value;
private final Candidate candidate;
public Match(V value, C candidate) {
public Match(Value value, Candidate candidate) {
this.value = value;
this.candidate = candidate;
}
public V getValue() {
public Value getValue() {
return value;
}
public C getCandidate() {
public Candidate getCandidate() {
return candidate;
}
@ -36,6 +36,23 @@ public class Match<V, C> {
}
@Override
public boolean equals(Object obj) {
if (obj instanceof Match) {
Match<?, ?> other = (Match<?, ?>) obj;
return value == other.value && candidate == other.candidate;
}
return false;
}
@Override
public int hashCode() {
return (value == null ? 0 : value.hashCode()) ^ (candidate == null ? 0 : candidate.hashCode());
}
@Override
public String toString() {
return String.format("[%s, %s]", value, candidate);

View File

@ -172,19 +172,12 @@ public class Matcher<V, C> {
protected static class DisjointMatchCollection<V, C> extends AbstractList<Match<V, C>> {
private final List<Match<V, C>> matches;
private final List<Match<V, C>> matches = new ArrayList<Match<V, C>>();
private final Map<V, Match<V, C>> values;
private final Map<C, Match<V, C>> candidates;
private final Map<V, Match<V, C>> values = new IdentityHashMap<V, Match<V, C>>();
private final Map<C, Match<V, C>> candidates = new IdentityHashMap<C, Match<V, C>>();
public DisjointMatchCollection() {
matches = new ArrayList<Match<V, C>>();
values = new IdentityHashMap<V, Match<V, C>>();
candidates = new IdentityHashMap<C, Match<V, C>>();
}
@Override
public boolean add(Match<V, C> match) {
if (disjoint(match)) {

View File

@ -23,8 +23,6 @@
package net.sourceforge.filebot.torrent;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
@ -37,10 +35,10 @@ import java.util.Map;
/**
* A set of utility methods to decode a bencoded array of byte into a Map. integer are represented as Long, String as byte[], dictionnaries as Map, and list as List.
* A set of utility methods to decode a bencoded array of byte into a Map. integer are
* represented as Long, String as byte[], dictionnaries as Map, and list as List.
*
* @author TdC_VgA
*
*/
public class BDecoder {
@ -49,34 +47,12 @@ public class BDecoder {
private static final Charset BINARY_CHARSET = Charset.forName("ISO-8859-1");
public static Map<?, ?> decode(byte[] data)
throws IOException {
return (new BDecoder().decodeByteArray(data));
}
public static Map<?, ?> decode(BufferedInputStream is)
throws IOException {
public static Map<?, ?> decode(InputStream is) throws IOException {
return (new BDecoder().decodeStream(is));
}
public BDecoder() {
}
public Map<?, ?> decodeByteArray(byte[] data)
throws IOException {
return (decode(new ByteArrayInputStream(data)));
}
public Map<?, ?> decodeStream(BufferedInputStream data)
throws IOException {
public Map<?, ?> decodeStream(InputStream data) throws IOException {
Object res = decodeInputStream(data, 0);
if (res == null)
@ -88,23 +64,7 @@ public class BDecoder {
}
private Map<?, ?> decode(ByteArrayInputStream data)
throws IOException {
Object res = decodeInputStream(data, 0);
if (res == null)
throw (new IOException("BDecoder: zero length file"));
else if (!(res instanceof Map))
throw (new IOException("BDecoder: top level isn't a Map"));
return ((Map<?, ?>) res);
}
private Object decodeInputStream(InputStream bais, int nesting)
throws IOException {
private Object decodeInputStream(InputStream bais, int nesting) throws IOException {
if (!bais.markSupported())
throw new IOException("InputStream must support the mark() method");
@ -252,7 +212,6 @@ public class BDecoder {
bais.skip(1);
// return the value
CharBuffer cb = BINARY_CHARSET.decode(ByteBuffer.wrap(tempArray));
String str_value = new String(cb.array(), 0, cb.limit());
@ -270,13 +229,13 @@ public class BDecoder {
// note that torrent hashes can be big (consider a 55GB file with 2MB
// pieces
// this generates a pieces hash of 1/2 meg
if (length > 8 * 1024 * 1024)
throw (new IOException("Byte array length too large (" + length + ")"));
byte[] tempArray = new byte[length];
int count = 0;
int len = 0;
// get the string
while ((count != length) && ((len = bais.read(tempArray, count, length - count)) > 0))
count += len;

View File

@ -2,10 +2,11 @@
package net.sourceforge.filebot.torrent;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.channels.FileChannel;
import java.nio.channels.FileChannel.MapMode;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collections;
@ -15,6 +16,8 @@ import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import net.sourceforge.tuned.ByteBufferInputStream;
public class Torrent {
@ -104,12 +107,13 @@ public class Torrent {
private static Map<?, ?> decodeTorrent(File torrent) throws IOException {
BufferedInputStream in = new BufferedInputStream(new FileInputStream(torrent));
FileChannel fileChannel = new FileInputStream(torrent).getChannel();
try {
return BDecoder.decode(in);
// memory-map and decode torrent
return BDecoder.decode(new ByteBufferInputStream(fileChannel.map(MapMode.READ_ONLY, 0, fileChannel.size())));
} finally {
in.close();
fileChannel.close();
}
}

View File

@ -7,7 +7,6 @@ import static java.awt.Font.MONOSPACED;
import static java.awt.Font.PLAIN;
import java.awt.Color;
import java.awt.Component;
import java.awt.Font;
import java.awt.Window;
import java.awt.event.ActionEvent;
@ -17,7 +16,6 @@ import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.text.Format;
import java.text.ParseException;
import java.util.Arrays;
import java.util.ResourceBundle;
@ -66,7 +64,7 @@ import net.sourceforge.tuned.ui.notification.SeparatorBorder.Position;
public class EpisodeFormatDialog extends JDialog {
private Format selectedFormat = null;
private Option selectedOption = null;
private JLabel preview = new JLabel();
@ -85,6 +83,13 @@ public class EpisodeFormatDialog extends JDialog {
private Color errorColor = Color.red;
public enum Option {
APPROVE,
CANCEL,
USE_DEFAULT
}
public EpisodeFormatDialog(Window owner) {
super(owner, "Episode Format", ModalityType.DOCUMENT_MODAL);
@ -123,7 +128,7 @@ public class EpisodeFormatDialog extends JDialog {
content.add(createExamplesPanel(), "gapx indent indent, wrap 25px:push");
content.add(new JButton(useDefaultFormatAction), "tag left");
content.add(new JButton(useCustomFormatAction), "tag apply");
content.add(new JButton(approveFormatAction), "tag apply");
content.add(new JButton(cancelAction), "tag cancel");
JComponent pane = (JComponent) getContentPane();
@ -312,69 +317,79 @@ public class EpisodeFormatDialog extends JDialog {
private void checkFormatInBackground() {
final Timer progressIndicatorTimer = TunedUtilities.invokeLater(400, new Runnable() {
try {
// check syntax in foreground
final ExpressionFormat format = new ExpressionFormat(editor.getText().trim());
@Override
public void run() {
progressIndicator.setVisible(true);
}
});
previewExecutor.execute(new SwingWorker<String, Void>() {
private ScriptException warning = null;
@Override
protected String doInBackground() throws Exception {
ExpressionFormat format = new ExpressionFormat(editor.getText().trim());
// format in background
final Timer progressIndicatorTimer = TunedUtilities.invokeLater(400, new Runnable() {
String text = format.format(previewSample);
warning = format.scriptException();
@Override
public void run() {
progressIndicator.setVisible(true);
}
});
previewExecutor.execute(new SwingWorker<String, Void>() {
// check if format produces empty strings
if (text.trim().isEmpty()) {
throw new IllegalArgumentException("Format must not be empty.");
@Override
protected String doInBackground() throws Exception {
return format.format(previewSample);
}
return text;
}
@Override
protected void done() {
Exception error = null;
try {
preview.setText(get());
} catch (Exception e) {
error = e;
@Override
protected void done() {
try {
preview.setText(get());
// check internal script exception and empty output
if (format.scriptException() != null) {
warningMessage.setText(format.scriptException().getCause().getMessage());
} else if (get().trim().isEmpty()) {
warningMessage.setText("Formatted value is empty");
} else {
warningMessage.setText(null);
}
} catch (Exception e) {
Logger.getLogger("global").log(Level.WARNING, e.getMessage(), e);
}
preview.setVisible(true);
warningMessage.setVisible(warningMessage.getText() != null);
errorMessage.setVisible(false);
editor.setForeground(defaultColor);
progressIndicatorTimer.stop();
progressIndicator.setVisible(false);
}
errorMessage.setText(error != null ? ExceptionUtilities.getRootCauseMessage(error) : null);
errorMessage.setVisible(error != null);
warningMessage.setText(warning != null ? warning.getCause().getMessage() : null);
warningMessage.setVisible(warning != null);
preview.setVisible(error == null);
editor.setForeground(error == null ? defaultColor : errorColor);
progressIndicatorTimer.stop();
progressIndicator.setVisible(false);
}
});
} catch (ScriptException e) {
// incorrect syntax
errorMessage.setText(ExceptionUtilities.getRootCauseMessage(e));
errorMessage.setVisible(true);
});
preview.setVisible(false);
warningMessage.setVisible(false);
editor.setForeground(errorColor);
}
}
public Format getSelectedFormat() {
return selectedFormat;
public String getExpression() {
return editor.getText();
}
private void finish(Format format) {
this.selectedFormat = format;
public Option getSelectedOption() {
return selectedOption;
}
private void finish(Option option) {
selectedOption = option;
previewExecutor.shutdownNow();
@ -386,7 +401,7 @@ public class EpisodeFormatDialog extends JDialog {
@Override
public void actionPerformed(ActionEvent e) {
finish(null);
finish(Option.CANCEL);
}
};
@ -394,17 +409,22 @@ public class EpisodeFormatDialog extends JDialog {
@Override
public void actionPerformed(ActionEvent e) {
finish(EpisodeFormat.getInstance());
finish(Option.USE_DEFAULT);
}
};
protected final Action useCustomFormatAction = new AbstractAction("Use Format", ResourceManager.getIcon("dialog.continue")) {
protected final Action approveFormatAction = new AbstractAction("Use Format", ResourceManager.getIcon("dialog.continue")) {
@Override
public void actionPerformed(ActionEvent evt) {
try {
finish(new ExpressionFormat(editor.getText()));
// check syntax
new ExpressionFormat(editor.getText());
// remember format
Settings.userRoot().put("dialog.format", editor.getText());
finish(Option.APPROVE);
} catch (ScriptException e) {
Logger.getLogger("ui").log(Level.WARNING, ExceptionUtilities.getRootCauseMessage(e), e);
}
@ -416,15 +436,6 @@ public class EpisodeFormatDialog extends JDialog {
firePropertyChange("previewSample", null, previewSample);
}
public static Format showDialog(Component parent) {
EpisodeFormatDialog dialog = new EpisodeFormatDialog(TunedUtilities.getWindow(parent));
dialog.setVisible(true);
return dialog.getSelectedFormat();
}
protected class ExampleFormatAction extends AbstractAction {

View File

@ -0,0 +1,44 @@
package net.sourceforge.filebot.ui.panel.rename;
import java.io.File;
import javax.script.ScriptException;
import net.sourceforge.filebot.format.EpisodeFormatBindingBean;
import net.sourceforge.filebot.format.ExpressionFormat;
import net.sourceforge.filebot.similarity.Match;
import net.sourceforge.filebot.web.Episode;
import net.sourceforge.filebot.web.Episode.EpisodeFormat;
public class EpisodeExpressionFormatter extends ExpressionFormat implements MatchFormatter {
public EpisodeExpressionFormatter(String expression) throws ScriptException {
super(expression);
}
@Override
public boolean canFormat(Match<?, ?> match) {
// episode is required, file is optional
return match.getValue() instanceof Episode && (match.getCandidate() == null || match.getCandidate() instanceof File);
}
@Override
public String preview(Match<?, ?> match) {
return EpisodeFormat.getInstance().format(match.getValue());
}
@Override
public String format(Match<?, ?> match) {
Episode episode = (Episode) match.getValue();
File mediaFile = (File) match.getCandidate();
return format(new EpisodeFormatBindingBean(episode, mediaFile));
}
}

View File

@ -0,0 +1,32 @@
package net.sourceforge.filebot.ui.panel.rename;
import java.io.File;
import net.sourceforge.filebot.similarity.Match;
import net.sourceforge.tuned.FileUtilities;
public class FileNameFormatter implements MatchFormatter {
@Override
public boolean canFormat(Match<?, ?> match) {
return match.getValue() instanceof File;
}
@Override
public String preview(Match<?, ?> match) {
return format(match);
}
@Override
public String format(Match<?, ?> match) {
File file = (File) match.getValue();
return FileUtilities.getName(file);
}
}

View File

@ -6,7 +6,6 @@ import static net.sourceforge.tuned.FileUtilities.FOLDERS;
import static net.sourceforge.tuned.FileUtilities.containsOnly;
import java.io.File;
import java.util.Arrays;
import java.util.List;
import net.sourceforge.filebot.ui.transfer.FileTransferablePolicy;
@ -39,17 +38,10 @@ class FilesListTransferablePolicy extends FileTransferablePolicy {
protected void load(List<File> files) {
if (containsOnly(files, FOLDERS)) {
for (File folder : files) {
loadFiles(Arrays.asList(folder.listFiles()));
model.addAll(FastFile.foreach(folder.listFiles()));
}
} else {
loadFiles(files);
}
}
protected void loadFiles(List<File> files) {
for (File file : files) {
model.add(new FastFile(file.getPath()));
model.addAll(FastFile.foreach(files));
}
}

View File

@ -40,12 +40,12 @@ import net.sourceforge.tuned.ui.ProgressDialog.Cancellable;
class MatchAction extends AbstractAction {
private final RenameModel<Object, File> model;
private final RenameModel model;
private final Collection<SimilarityMetric> metrics;
public MatchAction(RenameModel<Object, File> model) {
public MatchAction(RenameModel model) {
super("Match", ResourceManager.getIcon("action.match"));
this.model = model;
@ -138,10 +138,6 @@ class MatchAction extends AbstractAction {
public void actionPerformed(ActionEvent evt) {
if (model.names().isEmpty() || model.files().isEmpty()) {
return;
}
JComponent eventSource = (JComponent) evt.getSource();
SwingUtilities.getRoot(eventSource).setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
@ -192,9 +188,9 @@ class MatchAction extends AbstractAction {
private final Matcher<Object, File> matcher;
public BackgroundMatcher(RenameModel<Object, File> model, Collection<SimilarityMetric> metrics) {
public BackgroundMatcher(MatchModel<Object, File> model, Collection<SimilarityMetric> metrics) {
// match names against files
this.matcher = new Matcher<Object, File>(model.names(), model.files(), metrics);
this.matcher = new Matcher<Object, File>(model.values(), model.candidates(), metrics);
}
@ -215,14 +211,10 @@ class MatchAction extends AbstractAction {
model.clear();
// put new data into model
for (Match<Object, File> match : matches) {
model.names().add(match.getValue());
model.files().add(match.getCandidate());
}
model.addAll(matches);
// insert objects that could not be matched at the end
model.names().addAll(matcher.remainingValues());
model.files().addAll(matcher.remainingCandidates());
// insert objects that could not be matched at the end of the model
model.addAll(matcher.remainingValues(), matcher.remainingCandidates());
} catch (Exception e) {
Logger.getLogger(getClass().getName()).log(Level.SEVERE, e.toString(), e);
}

View File

@ -0,0 +1,18 @@
package net.sourceforge.filebot.ui.panel.rename;
import net.sourceforge.filebot.similarity.Match;
public interface MatchFormatter {
public boolean canFormat(Match<?, ?> match);
public String preview(Match<?, ?> match);
public String format(Match<?, ?> match);
}

View File

@ -0,0 +1,291 @@
package net.sourceforge.filebot.ui.panel.rename;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import net.sourceforge.filebot.similarity.Match;
import ca.odell.glazedlists.BasicEventList;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.TransformedList;
import ca.odell.glazedlists.event.ListEvent;
class MatchModel<Value, Candidate> {
private final EventList<Match<Value, Candidate>> source = new BasicEventList<Match<Value, Candidate>>();
private final EventList<Value> values;
private final EventList<Candidate> candidates;
public MatchModel() {
this.values = new MatchView<Value, Candidate>(source) {
@Override
public Value getElement(Match<Value, Candidate> match) {
return match.getValue();
}
@Override
public Candidate getComplement(Match<Value, Candidate> match) {
return match.getCandidate();
}
@Override
public Match<Value, Candidate> createMatch(Value element, Candidate complement) {
return new Match<Value, Candidate>(element, complement);
}
};
this.candidates = new MatchView<Candidate, Value>(source) {
@Override
public Candidate getElement(Match<Value, Candidate> match) {
return match.getCandidate();
}
@Override
public Value getComplement(Match<Value, Candidate> match) {
return match.getValue();
}
@Override
public Match<Value, Candidate> createMatch(Candidate element, Value complement) {
return new Match<Value, Candidate>(complement, element);
}
};
}
public void clear() {
source.clear();
}
public int size() {
return source.size();
}
public Match<Value, Candidate> getMatch(int index) {
return source.get(index);
}
public boolean hasComplement(int index) {
return source.get(index).getValue() != null && source.get(index).getCandidate() != null;
}
public EventList<Match<Value, Candidate>> matches() {
return source;
}
public EventList<Value> values() {
return values;
}
public EventList<Candidate> candidates() {
return candidates;
}
public void addAll(Collection<Match<Value, Candidate>> matches) {
source.addAll(matches);
}
public void addAll(Collection<Value> values, Collection<Candidate> candidates) {
if (this.values.size() != this.candidates.size())
throw new IllegalStateException("Existing matches are not balanced");
Iterator<Value> valueIterator = values.iterator();
Iterator<Candidate> candidateIterator = candidates.iterator();
while (valueIterator.hasNext() || candidateIterator.hasNext()) {
Value value = valueIterator.hasNext() ? valueIterator.next() : null;
Candidate candidate = candidateIterator.hasNext() ? candidateIterator.next() : null;
source.add(new Match<Value, Candidate>(value, candidate));
}
}
private abstract class MatchView<Element, Complement> extends TransformedList<Match<Value, Candidate>, Element> {
public MatchView(EventList<Match<Value, Candidate>> source) {
super(source);
source.addListEventListener(this);
}
public abstract Element getElement(Match<Value, Candidate> match);
public abstract Complement getComplement(Match<Value, Candidate> match);
public abstract Match<Value, Candidate> createMatch(Element element, Complement complement);
@Override
public Element get(int index) {
return getElement(index);
}
public Element getElement(int index) {
return getElement(source.get(index));
}
public Complement getComplement(int index) {
return getComplement(source.get(index));
}
@Override
public boolean addAll(Collection<? extends Element> values) {
return put(size(), values);
}
@Override
public boolean add(Element value) {
return put(size(), Collections.singleton(value));
};
@Override
public void add(int index, Element value) {
List<Element> range = new ArrayList<Element>();
range.add(value);
range.addAll(subList(index, size()));
put(index, range);
}
@Override
public Element remove(int index) {
Element old = getElement(index);
int lastIndex = size() - 1;
// shift subsequent elements
put(index, new ArrayList<Element>(subList(index + 1, lastIndex + 1)));
// remove last element
if (getComplement(lastIndex) == null) {
source.remove(lastIndex);
} else {
set(lastIndex, null);
}
return old;
}
@Override
public Element set(int index, Element element) {
Element old = getElement(index);
source.set(index, createMatch(element, getComplement(index)));
return old;
}
@Override
public void clear() {
// remove in reverse, because null matches may only
// exist at the and of the source model
for (int i = size() - 1; i >= 0; i--) {
Complement complement = getComplement(i);
if (complement != null) {
// replace original match with null match
source.set(i, createMatch(null, complement));
} else {
// remove match if value and candidate are null
source.remove(i);
}
}
}
private boolean put(int index, Collection<? extends Element> elements) {
for (Element element : elements) {
if (index < source.size()) {
set(index, element);
} else {
source.add(index, createMatch(element, null));
}
index++;
}
return true;
}
@Override
protected boolean isWritable() {
// can't write to source directly
return false;
}
private int size = 0;
@Override
public int size() {
return size;
}
@Override
public void listChanged(ListEvent<Match<Value, Candidate>> listChanges) {
updates.beginEvent(true);
while (listChanges.next()) {
int index = listChanges.getIndex();
int type = listChanges.getType();
if (type == ListEvent.INSERT || type == ListEvent.UPDATE) {
if (index < size) {
if (index == size - 1 && getElement(index) == null) {
updates.elementDeleted(index, null);
size--;
} else {
updates.elementUpdated(index, null, getElement(index));
}
} else if (index == size && getElement(index) != null) {
updates.elementInserted(index, getElement(index));
size++;
}
} else if (type == ListEvent.DELETE && index < size) {
updates.elementDeleted(index, null);
size--;
}
}
updates.commitEvent();
}
}
}

View File

@ -17,7 +17,6 @@ import java.io.FileNotFoundException;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Scanner;
import java.util.logging.Level;
@ -27,6 +26,7 @@ import net.sourceforge.filebot.torrent.Torrent;
import net.sourceforge.filebot.ui.transfer.ArrayTransferable;
import net.sourceforge.filebot.ui.transfer.FileTransferablePolicy;
import net.sourceforge.filebot.web.Episode;
import net.sourceforge.tuned.FastFile;
class NamesListTransferablePolicy extends FileTransferablePolicy {
@ -106,10 +106,10 @@ class NamesListTransferablePolicy extends FileTransferablePolicy {
} else if (containsOnly(files, FOLDERS)) {
// load files from each folder
for (File folder : files) {
Collections.addAll(values, folder.listFiles());
values.addAll(FastFile.foreach(folder.listFiles()));
}
} else {
values.addAll(files);
values.addAll(FastFile.foreach(files));
}
model.addAll(values);

View File

@ -1,172 +0,0 @@
package net.sourceforge.filebot.ui.panel.rename;
import static net.sourceforge.filebot.FileBotUtilities.isInvalidFileName;
import java.awt.Component;
import java.text.Format;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.TransformedList;
import ca.odell.glazedlists.event.ListEvent;
public class NamesViewEventList extends TransformedList<Object, String> {
private final List<String> names = new ArrayList<String>();
private final Map<Class<?>, Format> formatMap = new HashMap<Class<?>, Format>();
private final Component parent;
public NamesViewEventList(Component parent, EventList<Object> source) {
super(source);
this.parent = parent;
// connect to source list
source.addListEventListener(this);
}
@Override
protected boolean isWritable() {
return true;
}
@Override
public String get(int index) {
return names.get(index);
}
public void setFormat(Class<?> type, Format format) {
if (format != null) {
// insert new format for type
formatMap.put(type, format);
} else {
// restore default format for type
formatMap.remove(type);
}
updates.beginEvent(true);
List<Integer> changes = new ArrayList<Integer>();
// reformat all elements of the source list
for (int i = 0; i < source.size(); i++) {
String newValue = format(source.get(i));
String oldValue = names.set(i, newValue);
if (!newValue.equals(oldValue)) {
updates.elementUpdated(i, oldValue, newValue);
changes.add(i);
}
}
submit(new IndexView<String>(names, changes));
updates.commitEvent();
}
private String format(Object object) {
for (Entry<Class<?>, Format> entry : formatMap.entrySet()) {
if (entry.getKey().isInstance(object)) {
return entry.getValue().format(object);
}
}
return object.toString();
}
@Override
public void listChanged(ListEvent<Object> listChanges) {
EventList<Object> source = listChanges.getSourceList();
List<Integer> changes = new ArrayList<Integer>();
while (listChanges.next()) {
int index = listChanges.getIndex();
int type = listChanges.getType();
switch (type) {
case ListEvent.INSERT:
names.add(index, format(source.get(index)));
changes.add(index);
break;
case ListEvent.UPDATE:
names.set(index, format(source.get(index)));
changes.add(index);
break;
case ListEvent.DELETE:
names.remove(index);
break;
}
}
submit(new IndexView<String>(names, changes));
listChanges.reset();
updates.forwardEvent(listChanges);
}
protected void submit(List<String> values) {
List<Integer> issues = new ArrayList<Integer>();
for (int i = 0; i < values.size(); i++) {
if (isInvalidFileName(values.get(i))) {
issues.add(i);
}
}
if (issues.size() > 0) {
// validate names
ValidateNamesDialog.showDialog(parent, new IndexView<String>(values, issues));
}
}
protected static class IndexView<E> extends AbstractList<E> {
private final List<E> source;
private final List<Integer> filter;
public IndexView(List<E> source, List<Integer> filter) {
this.source = source;
this.filter = filter;
}
@Override
public E get(int index) {
return source.get(filter.get(index));
}
@Override
public E set(int index, E element) {
return source.set(filter.get(index), element);
};
@Override
public int size() {
return filter.size();
}
}
}

View File

@ -19,10 +19,10 @@ import net.sourceforge.tuned.FileUtilities;
class RenameAction extends AbstractAction {
private final RenameModel<String, File> model;
private final RenameModel model;
public RenameAction(RenameModel<String, File> model) {
public RenameAction(RenameModel model) {
super("Rename", ResourceManager.getIcon("action.rename"));
putValue(SHORT_DESCRIPTION, "Rename files");
@ -32,11 +32,10 @@ class RenameAction extends AbstractAction {
public void actionPerformed(ActionEvent evt) {
Deque<Match<File, File>> todoQueue = new ArrayDeque<Match<File, File>>();
Deque<Match<File, File>> doneQueue = new ArrayDeque<Match<File, File>>();
for (Match<String, File> match : model.matches()) {
for (Match<String, File> match : model.getMatchesForRenaming()) {
File source = match.getCandidate();
String extension = FileUtilities.getExtension(source);

View File

@ -59,14 +59,13 @@ class RenameList<E> extends FileBotList<E> {
}
protected boolean moveEntry(int fromIndex, int toIndex) {
if (toIndex < 0 || toIndex >= getModel().size())
return false;
public void swap(int index1, int index2) {
E e1 = model.get(index1);
E e2 = model.get(index2);
// move element
getModel().add(toIndex, getModel().remove(fromIndex));
return true;
// swap data
model.set(index1, e2);
model.set(index2, e1);
}
private final LoadAction loadAction = new LoadAction(null);
@ -76,7 +75,8 @@ class RenameList<E> extends FileBotList<E> {
public void actionPerformed(ActionEvent e) {
int index = getListComponent().getSelectedIndex();
if (moveEntry(index, index - 1)) {
if (index > 0) {
swap(index, index - 1);
getListComponent().setSelectedIndex(index - 1);
}
}
@ -87,7 +87,8 @@ class RenameList<E> extends FileBotList<E> {
public void actionPerformed(ActionEvent e) {
int index = getListComponent().getSelectedIndex();
if (moveEntry(index, index + 1)) {
if (index < model.size() - 1) {
swap(index, index + 1);
getListComponent().setSelectedIndex(index + 1);
}
}
@ -95,25 +96,23 @@ class RenameList<E> extends FileBotList<E> {
private final MouseAdapter dndReorderMouseAdapter = new MouseAdapter() {
private int fromIndex = -1;
private int lastIndex = -1;
@Override
public void mousePressed(MouseEvent m) {
fromIndex = getListComponent().getSelectedIndex();
lastIndex = getListComponent().getSelectedIndex();
}
@Override
public void mouseDragged(MouseEvent m) {
int toIndex = getListComponent().getSelectedIndex();
int currentIndex = getListComponent().getSelectedIndex();
if (toIndex == fromIndex)
return;
moveEntry(fromIndex, toIndex);
fromIndex = toIndex;
if (currentIndex != lastIndex) {
swap(lastIndex, currentIndex);
lastIndex = currentIndex;
}
}
};

View File

@ -12,62 +12,77 @@ import java.awt.geom.Rectangle2D;
import java.awt.geom.RoundRectangle2D;
import java.io.File;
import javax.swing.Box;
import javax.swing.BoxLayout;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.border.CompoundBorder;
import javax.swing.border.EmptyBorder;
import net.miginfocom.swing.MigLayout;
import net.sourceforge.filebot.ResourceManager;
import net.sourceforge.filebot.similarity.Match;
import net.sourceforge.filebot.ui.panel.rename.RenameModel.FormattedFuture;
import net.sourceforge.filebot.web.Episode;
import net.sourceforge.tuned.FileUtilities;
import net.sourceforge.tuned.ui.DefaultFancyListCellRenderer;
class RenameListCellRenderer extends DefaultFancyListCellRenderer {
private final RenameModel<?, ?> model;
private final RenameModel renameModel;
private final ExtensionLabel extension = new ExtensionLabel();
public RenameListCellRenderer(RenameModel<?, ?> model) {
this.model = model;
setHighlightingEnabled(false);
setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
add(Box.createHorizontalGlue());
add(extension);
}
private final TypeLabel typeLabel = new TypeLabel();
private final Color noMatchGradientBeginColor = new Color(0xB7B7B7);
private final Color noMatchGradientEndColor = new Color(0x9A9A9A);
public RenameListCellRenderer(RenameModel renameModel) {
this.renameModel = renameModel;
setHighlightingEnabled(false);
setLayout(new MigLayout("fill, insets 0", "align left", "align center"));
add(typeLabel, "gap rel:push");
}
@Override
public void configureListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
super.configureListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
// show extension label only for items of the files model
// reset
setIcon(null);
typeLabel.setText(null);
typeLabel.setAlpha(1.0f);
if (value instanceof File) {
// display file extension
File file = (File) value;
this.setText(FileUtilities.getName(file));
setText(FileUtilities.getName(file));
typeLabel.setText(getType(file));
} else if (value instanceof FormattedFuture) {
// progress icon and value type
FormattedFuture future = (FormattedFuture) value;
extension.setText(getType(file));
extension.setAlpha(1.0f);
switch (future.getState()) {
case PENDING:
setIcon(ResourceManager.getIcon("worker.pending"));
break;
case STARTED:
setIcon(ResourceManager.getIcon("worker.started"));
break;
}
extension.setVisible(true);
} else {
extension.setVisible(false);
typeLabel.setText(getType(future.getMatch()));
}
if (index >= model.matchCount()) {
if (!renameModel.hasComplement(index)) {
if (isSelected) {
setGradientColors(noMatchGradientBeginColor, noMatchGradientEndColor);
} else {
setForeground(noMatchGradientBeginColor);
extension.setAlpha(0.5f);
typeLabel.setAlpha(0.5f);
}
}
}
@ -86,8 +101,23 @@ class RenameListCellRenderer extends DefaultFancyListCellRenderer {
return "File";
}
protected String getType(Match<Object, File> match) {
Object source = match.getValue();
if (source instanceof Episode) {
return "Episode";
} else if (source instanceof AbstractFileEntry) {
return "Torrent";
} else if (source instanceof File) {
return "File";
}
return null;
}
protected class ExtensionLabel extends JLabel {
private class TypeLabel extends JLabel {
private final Insets margin = new Insets(0, 10, 0, 0);
private final Insets padding = new Insets(0, 6, 0, 5);
@ -99,7 +129,7 @@ class RenameListCellRenderer extends DefaultFancyListCellRenderer {
private float alpha = 1.0f;
public ExtensionLabel() {
public TypeLabel() {
setOpaque(false);
setForeground(new Color(0x141414));
@ -128,6 +158,15 @@ class RenameListCellRenderer extends DefaultFancyListCellRenderer {
}
@Override
public void setText(String text) {
super.setText(text);
// auto-hide if text is null
setVisible(text != null);
}
public void setAlpha(float alpha) {
this.alpha = alpha;
}

View File

@ -2,81 +2,309 @@
package net.sourceforge.filebot.ui.panel.rename;
import java.util.AbstractList;
import java.util.Collection;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.SwingWorker;
import javax.swing.SwingWorker.StateValue;
import net.sourceforge.filebot.similarity.Match;
import ca.odell.glazedlists.BasicEventList;
import net.sourceforge.tuned.ui.TunedUtilities;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.TransformedList;
import ca.odell.glazedlists.event.ListEvent;
class RenameModel<N, V> {
public class RenameModel extends MatchModel<Object, File> {
private final EventList<N> names;
private final EventList<V> files;
private final FormattedFutureEventList names = new FormattedFutureEventList();
private final Map<Class<?>, MatchFormatter> formatters = new HashMap<Class<?>, MatchFormatter>();
private final MatchFormatter defaultFormatter = new MatchFormatter() {
@Override
public boolean canFormat(Match<?, ?> match) {
return true;
}
@Override
public String preview(Match<?, ?> match) {
return format(match);
}
@Override
public String format(Match<?, ?> match) {
return String.valueOf(match.getValue());
}
};
public RenameModel(EventList<N> names, EventList<V> files) {
this.names = names;
this.files = files;
public void useFormatter(Class<?> type, MatchFormatter formatter) {
if (formatter != null) {
formatters.put(type, formatter);
} else {
formatters.remove(type);
}
// reformat matches
names.refresh();
}
public EventList<N> names() {
public EventList<FormattedFuture> names() {
return names;
}
public EventList<V> files() {
return files;
public EventList<File> files() {
return candidates();
}
public void clear() {
names.clear();
files.clear();
}
public int matchCount() {
return Math.min(names.size(), files.size());
}
public Match<N, V> getMatch(int index) {
if (index >= matchCount())
throw new IndexOutOfBoundsException();
public List<Match<String, File>> getMatchesForRenaming() {
List<Match<String, File>> matches = new ArrayList<Match<String, File>>();
return new Match<N, V>(names.get(index), files.get(index));
for (int i = 0; i < size(); i++) {
if (hasComplement(i) && names.get(i).isDone()) {
matches.add(new Match<String, File>(names().get(i).toString(), files().get(i)));
}
}
return matches;
}
public Collection<Match<N, V>> matches() {
return new AbstractList<Match<N, V>>() {
@Override
public Match<N, V> get(int index) {
return getMatch(index);
private MatchFormatter getFormatter(Match<Object, File> match) {
for (MatchFormatter formatter : formatters.values()) {
if (formatter.canFormat(match)) {
return formatter;
}
}
return defaultFormatter;
}
private class FormattedFutureEventList extends TransformedList<Object, FormattedFuture> {
private final List<FormattedFuture> futures = new ArrayList<FormattedFuture>();
private final Executor backgroundFormatter = new ThreadPoolExecutor(0, 1, 5L, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>());
public FormattedFutureEventList() {
super(values());
source.addListEventListener(this);
}
@Override
public int size() {
return matchCount();
@Override
public FormattedFuture get(int index) {
return futures.get(index);
}
@Override
protected boolean isWritable() {
// can't write to source directly
return false;
}
@Override
public void add(int index, FormattedFuture value) {
source.add(index, value.getMatch().getValue());
}
@Override
public FormattedFuture set(int index, FormattedFuture value) {
FormattedFuture obsolete = get(index);
source.set(index, value.getMatch().getValue());
return obsolete;
}
@Override
public FormattedFuture remove(int index) {
FormattedFuture obsolete = get(index);
source.remove(index);
return obsolete;
}
@Override
public void listChanged(ListEvent<Object> listChanges) {
updates.beginEvent(true);
while (listChanges.next()) {
int index = listChanges.getIndex();
int type = listChanges.getType();
if (type == ListEvent.INSERT || type == ListEvent.UPDATE) {
Match<Object, File> match = getMatch(index);
// create new future
final FormattedFuture future = new FormattedFuture(match, getFormatter(match));
// update data
if (type == ListEvent.INSERT) {
futures.add(index, future);
updates.elementInserted(index, future);
} else if (type == ListEvent.UPDATE) {
// set new future, dispose old future
FormattedFuture obsolete = futures.set(index, future);
cancel(obsolete);
// Don't update view immediately, to avoid irritating flickering,
// caused by a rapid succession of change events.
// The worker may only need a couple of milliseconds to complete,
// so the view will be notified of the change soon enough.
TunedUtilities.invokeLater(50, new Runnable() {
@Override
public void run() {
// task has not been started, no change events have been sent as of yet,
// fire change event now
if (future.getState() == StateValue.PENDING) {
future.firePropertyChange("state", null, StateValue.PENDING);
}
}
});
}
// observe and enqueue worker task
submit(future);
} else if (type == ListEvent.DELETE) {
// remove future from data and formatter queue
FormattedFuture obsolete = futures.remove(index);
cancel(obsolete);
updates.elementDeleted(index, obsolete);
}
}
updates.commitEvent();
}
public void refresh() {
updates.beginEvent(true);
for (int i = 0; i < size(); i++) {
FormattedFuture obsolete = futures.get(i);
FormattedFuture future = new FormattedFuture(obsolete.getMatch(), getFormatter(obsolete.getMatch()));
// replace and cancel old future
cancel(futures.set(i, future));
// submit new future
submit(future);
updates.elementUpdated(i, obsolete, future);
}
updates.commitEvent();
}
private void submit(FormattedFuture future) {
// observe and enqueue worker task
future.addPropertyChangeListener(futureListener);
backgroundFormatter.execute(future);
}
private void cancel(FormattedFuture future) {
// remove listener and cancel worker task
future.removePropertyChangeListener(futureListener);
future.cancel(true);
}
private final PropertyChangeListener futureListener = new PropertyChangeListener() {
public void propertyChange(PropertyChangeEvent evt) {
int index = futures.indexOf(evt.getSource());
// sanity check
if (index >= 0 && index < size()) {
FormattedFuture future = (FormattedFuture) evt.getSource();
updates.beginEvent(true);
updates.elementUpdated(index, future, future);
updates.commitEvent();
}
}
};
}
@SuppressWarnings("unchecked")
public static <S, V> RenameModel<S, V> create() {
return new RenameModel<S, V>((EventList<S>) new BasicEventList<Object>(), (EventList<V>) new BasicEventList<Object>());
}
public static class FormattedFuture extends SwingWorker<String, Void> {
private final Match<Object, File> match;
private final MatchFormatter formatter;
private String display;
private FormattedFuture(Match<Object, File> match, MatchFormatter formatter) {
this.match = match;
this.formatter = formatter;
// initial display value
this.display = formatter.preview(match);
}
public static <S, V> RenameModel<S, V> wrap(EventList<S> names, EventList<V> values) {
return new RenameModel<S, V>(names, values);
public Match<Object, File> getMatch() {
return match;
}
@Override
protected String doInBackground() throws Exception {
return formatter.format(match);
}
@Override
protected void done() {
if (isCancelled()) {
return;
}
try {
this.display = get();
} catch (Exception e) {
Logger.getLogger("global").log(Level.WARNING, e.getMessage(), e);
}
}
@Override
public String toString() {
return display;
}
}
}

View File

@ -9,7 +9,6 @@ import java.awt.event.ActionEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.text.Format;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
@ -19,7 +18,9 @@ import java.util.concurrent.FutureTask;
import java.util.concurrent.RunnableFuture;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.Preferences;
import javax.script.ScriptException;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.DefaultListSelectionModel;
@ -33,12 +34,12 @@ import javax.swing.SwingUtilities;
import net.miginfocom.swing.MigLayout;
import net.sourceforge.filebot.ResourceManager;
import net.sourceforge.filebot.Settings;
import net.sourceforge.filebot.format.EpisodeExpressionFormat;
import net.sourceforge.filebot.similarity.Match;
import net.sourceforge.filebot.similarity.NameSimilarityMetric;
import net.sourceforge.filebot.similarity.SimilarityMetric;
import net.sourceforge.filebot.ui.EpisodeFormatDialog;
import net.sourceforge.filebot.ui.SelectDialog;
import net.sourceforge.filebot.ui.panel.rename.RenameModel.FormattedFuture;
import net.sourceforge.filebot.web.AnidbClient;
import net.sourceforge.filebot.web.Episode;
import net.sourceforge.filebot.web.EpisodeListProvider;
@ -48,7 +49,7 @@ import net.sourceforge.filebot.web.TVDotComClient;
import net.sourceforge.filebot.web.TVRageClient;
import net.sourceforge.filebot.web.TheTVDBClient;
import net.sourceforge.tuned.ExceptionUtilities;
import net.sourceforge.tuned.FileUtilities.NameWithoutExtensionFormat;
import net.sourceforge.tuned.PreferencesMap.AbstractAdapter;
import net.sourceforge.tuned.PreferencesMap.PreferencesEntry;
import net.sourceforge.tuned.ui.ActionPopup;
import net.sourceforge.tuned.ui.LoadingOverlayPane;
@ -58,35 +59,31 @@ import ca.odell.glazedlists.event.ListEventListener;
public class RenamePanel extends JComponent {
protected final RenameModel<Object, File> model = RenameModel.create();
protected final RenameModel renameModel = new RenameModel();
protected final NamesViewEventList namesView = new NamesViewEventList(this, model.names());
protected final RenameList<FormattedFuture> namesList = new RenameList<FormattedFuture>(renameModel.names());
protected final RenameList<String> namesList = new RenameList<String>(namesView);
protected final RenameList<File> filesList = new RenameList<File>(renameModel.files());
protected final RenameList<File> filesList = new RenameList<File>(model.files());
protected final MatchAction matchAction = new MatchAction(renameModel);
protected final MatchAction matchAction = new MatchAction(model);
protected final RenameAction renameAction = new RenameAction(RenameModel.wrap(namesView, model.files()));
protected final PreferencesEntry<String> persistentFormat = Settings.userRoot().entry("rename.format");
protected final RenameAction renameAction = new RenameAction(renameModel);
public RenamePanel() {
namesList.setTitle("New Names");
namesList.setTransferablePolicy(new NamesListTransferablePolicy(renameModel.values()));
namesList.setTitle("Proposed");
namesList.setTransferablePolicy(new NamesListTransferablePolicy(model.names()));
filesList.setTitle("Original Files");
filesList.setTransferablePolicy(new FilesListTransferablePolicy(renameModel.files()));
filesList.setTitle("Current");
filesList.setTransferablePolicy(new FilesListTransferablePolicy(filesList.getModel()));
// filename formatter
renameModel.useFormatter(File.class, new FileNameFormatter());
namesView.setFormat(File.class, new NameWithoutExtensionFormat());
// custom episode formatter, if any
renameModel.useFormatter(Episode.class, persistentFormatExpression.getValue());
// restore custom format
restoreEpisodeFormat();
RenameListCellRenderer cellrenderer = new RenameListCellRenderer(model);
RenameListCellRenderer cellrenderer = new RenameListCellRenderer(renameModel);
namesList.getListComponent().setCellRenderer(cellrenderer);
filesList.getListComponent().setCellRenderer(cellrenderer);
@ -120,7 +117,7 @@ public class RenamePanel extends JComponent {
setLayout(new MigLayout("fill, insets dialog, gapx 10px", "[fill][align center, pref!][fill]", "align 33%"));
add(new LoadingOverlayPane(namesList, namesList, "28px", "30px"), "grow, sizegroupx list");
add(filesList, "grow, sizegroupx list");
// make buttons larger
matchButton.setMargin(new Insets(3, 14, 2, 14));
@ -129,11 +126,11 @@ public class RenamePanel extends JComponent {
add(matchButton, "split 2, flowy, sizegroupx button");
add(renameButton, "gapy 30px, sizegroupx button");
add(filesList, "grow, sizegroupx list");
add(new LoadingOverlayPane(namesList, namesList, "28px", "30px"), "grow, sizegroupx list");
// repaint on change
model.names().addListEventListener(new RepaintHandler<Object>());
model.files().addListEventListener(new RepaintHandler<File>());
renameModel.names().addListEventListener(new RepaintHandler<Object>());
renameModel.files().addListEventListener(new RepaintHandler<Object>());
}
@ -153,17 +150,26 @@ public class RenamePanel extends JComponent {
actionPopup.add(new AbstractAction("Edit Format", ResourceManager.getIcon("action.format")) {
@Override
public void actionPerformed(ActionEvent e) {
Format format = EpisodeFormatDialog.showDialog(RenamePanel.this);
public void actionPerformed(ActionEvent evt) {
EpisodeFormatDialog dialog = new EpisodeFormatDialog(SwingUtilities.getWindowAncestor(RenamePanel.this));
if (format != null) {
if (format instanceof EpisodeExpressionFormat) {
persistentFormat.setValue(((EpisodeExpressionFormat) format).getFormat());
} else {
persistentFormat.remove();
}
namesView.setFormat(Episode.class, format);
dialog.setVisible(true);
switch (dialog.getSelectedOption()) {
case APPROVE:
try {
EpisodeExpressionFormatter formatter = new EpisodeExpressionFormatter(dialog.getExpression());
renameModel.useFormatter(Episode.class, formatter);
persistentFormatExpression.setValue(formatter);
} catch (ScriptException e) {
// will not happen because illegal expressions cannot be approved in dialog
Logger.getLogger("ui").log(Level.WARNING, e.getMessage(), e);
}
break;
case USE_DEFAULT:
renameModel.useFormatter(Episode.class, null);
persistentFormatExpression.remove();
break;
}
}
});
@ -171,25 +177,12 @@ public class RenamePanel extends JComponent {
return actionPopup;
}
private void restoreEpisodeFormat() {
String format = persistentFormat.getValue();
if (format != null) {
try {
namesView.setFormat(Episode.class, new EpisodeExpressionFormat(format));
} catch (Exception e) {
Logger.getLogger("ui").log(Level.WARNING, e.getMessage(), e);
}
}
}
protected final Action showPopupAction = new AbstractAction("Show Popup") {
@Override
public void actionPerformed(ActionEvent e) {
// show popup on actionPerformed only when names list is empty
if (model.names().isEmpty() && !model.files().isEmpty()) {
if (renameModel.size() > 0 && !renameModel.hasComplement(0)) {
JComponent source = (JComponent) e.getSource();
// display popup below component
@ -227,28 +220,25 @@ public class RenamePanel extends JComponent {
namesList.firePropertyChange(LOADING_PROPERTY, false, true);
// clear names list
model.names().clear();
renameModel.values().clear();
AutoFetchEpisodeListMatcher worker = new AutoFetchEpisodeListMatcher(provider, model.files(), matchAction.getMetrics()) {
AutoFetchEpisodeListMatcher worker = new AutoFetchEpisodeListMatcher(provider, renameModel.files(), matchAction.getMetrics()) {
@Override
protected void done() {
try {
List<Episode> episodes = new ArrayList<Episode>();
List<File> files = new ArrayList<File>();
List<Match<Object, File>> matches = new ArrayList<Match<Object, File>>();
for (Match<File, Episode> match : get()) {
episodes.add(match.getCandidate());
files.add(match.getValue());
matches.add(new Match<Object, File>(match.getCandidate(), match.getValue()));
}
model.clear();
renameModel.clear();
model.names().addAll(episodes);
model.files().addAll(files);
renameModel.addAll(matches);
// add remaining file entries
model.files().addAll(remainingFiles());
renameModel.files().addAll(remainingFiles());
} catch (Exception e) {
Logger.getLogger("ui").log(Level.WARNING, ExceptionUtilities.getRootCauseMessage(e), e);
} finally {
@ -320,4 +310,28 @@ public class RenamePanel extends JComponent {
};
protected final PreferencesEntry<EpisodeExpressionFormatter> persistentFormatExpression = Settings.userRoot().entry("rename.format", new AbstractAdapter<EpisodeExpressionFormatter>() {
@Override
public EpisodeExpressionFormatter get(Preferences prefs, String key) {
String expression = prefs.get(key, null);
if (expression != null) {
try {
return new EpisodeExpressionFormatter(expression);
} catch (Exception e) {
Logger.getLogger("ui").log(Level.WARNING, e.getMessage(), e);
}
}
return null;
}
@Override
public void put(Preferences prefs, String key, EpisodeExpressionFormatter value) {
prefs.put(key, value.getExpression());
}
});
}

View File

@ -78,22 +78,15 @@ public class Episode implements Serializable {
sb.append(episode.getSeasonNumber()).append('x');
}
sb.append(formatEpisodeNumber(episode.getEpisodeNumber()));
return sb.append(" - ").append(episode.getTitle());
}
protected String formatEpisodeNumber(String number) {
if (number.length() < 2) {
try {
return String.format("%02d", Integer.parseInt(number));
} catch (NumberFormatException e) {
// ignore
}
try {
// try to format episode number
sb.append(String.format("%02d", Integer.parseInt(episode.getEpisodeNumber())));
} catch (NumberFormatException e) {
// use episode "number" as is
sb.append(episode.getEpisodeNumber());
}
return number;
return sb.append(" - ").append(episode.getTitle());
}

View File

@ -3,6 +3,9 @@ package net.sourceforge.tuned;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class FastFile extends File {
@ -52,4 +55,19 @@ public class FastFile extends File {
return files;
}
public static List<FastFile> foreach(File... files) {
return foreach(Arrays.asList(files));
}
public static List<FastFile> foreach(final List<File> files) {
List<FastFile> result = new ArrayList<FastFile>(files.size());
for (File file : files) {
result.add(new FastFile(file.getPath()));
}
return result;
}
}

View File

@ -4,9 +4,6 @@ package net.sourceforge.tuned;
import java.io.File;
import java.io.FileFilter;
import java.text.FieldPosition;
import java.text.Format;
import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.List;
@ -163,26 +160,6 @@ public final class FileUtilities {
}
}
public static class NameWithoutExtensionFormat extends Format {
@Override
public StringBuffer format(Object obj, StringBuffer sb, FieldPosition pos) {
if (obj instanceof File) {
return sb.append(getName((File) obj));
}
return sb.append(getNameWithoutExtension(obj.toString()));
}
@Override
public Object parseObject(String source, ParsePosition pos) {
throw new UnsupportedOperationException();
}
}
/**
* Dummy constructor to prevent instantiation.

View File

@ -2,7 +2,10 @@
package net.sourceforge.filebot;
import net.sourceforge.filebot.format.ExpressionFormatTest;
import net.sourceforge.filebot.similarity.SimilarityTestSuite;
import net.sourceforge.filebot.ui.panel.rename.MatchModelTest;
import net.sourceforge.filebot.ui.panel.sfv.VerificationFileScannerTest;
import net.sourceforge.filebot.web.WebTestSuite;
import org.junit.runner.RunWith;
@ -11,7 +14,7 @@ import org.junit.runners.Suite.SuiteClasses;
@RunWith(Suite.class)
@SuiteClasses( { SimilarityTestSuite.class, WebTestSuite.class, MiscSuite.class })
@SuiteClasses( { SimilarityTestSuite.class, WebTestSuite.class, ArgumentBeanTest.class, ExpressionFormatTest.class, VerificationFileScannerTest.class, MatchModelTest.class })
public class FileBotTestSuite {
}

View File

@ -1,15 +0,0 @@
package net.sourceforge.filebot;
import net.sourceforge.filebot.ui.panel.sfv.VerificationFileScannerTest;
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
@RunWith(Suite.class)
@SuiteClasses( { ArgumentBeanTest.class, VerificationFileScannerTest.class })
public class MiscSuite {
}

View File

@ -0,0 +1,105 @@
package net.sourceforge.filebot.ui.panel.rename;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import net.sourceforge.filebot.similarity.Match;
import org.junit.Test;
import ca.odell.glazedlists.GlazedLists;
public class MatchModelTest {
@Test
public void addAll() {
MatchModel<String, Integer> model = new MatchModel<String, Integer>();
List<String> names = Arrays.asList("A", "B", "C", "D", "E");
List<Integer> values = Arrays.asList(1, 2, 3);
model.addAll(Arrays.asList("A", "B", "C", "D", "E"), Arrays.asList(1, 2, 3));
assertEquals(5, model.size(), 0);
for (int i = 0; i < model.size(); i++) {
String name = i < names.size() ? names.get(i) : null;
Integer value = i < values.size() ? values.get(i) : null;
// check model and views
assertMatchEquals(name, value, model.matches().get(i));
assertEquals(name, model.values().get(i));
assertEquals(value, model.candidates().get(i));
}
}
@Test
public void matchViewElements() {
MatchModel<String, Integer> model = new MatchModel<String, Integer>();
model.addAll(Arrays.asList("A", "B", "C"), Arrays.asList(1, 2, 3, 4, 5));
model.values().add("D");
assertMatchEquals("D", 4, model.getMatch(3));
model.values().add(1, "A2");
assertMatchEquals("C", 4, model.getMatch(3));
model.candidates().remove(3);
assertMatchEquals("C", 5, model.getMatch(3));
model.candidates().remove(3);
assertMatchEquals("C", null, model.getMatch(3));
model.matches().remove(0);
assertMatchEquals("A2", 2, model.getMatch(0));
model.values().set(0, "A");
assertMatchEquals("A", 2, model.getMatch(0));
}
@Test
public void matchViewClear() {
MatchModel<String, Integer> model = new MatchModel<String, Integer>();
model.values().addAll(Arrays.asList("A", "B", "C"));
model.candidates().addAll(Arrays.asList(1, 2, 3, 4, 5));
model.values().clear();
assertEquals(0, model.values().size(), 0);
assertEquals(5, model.candidates().size(), 0);
model.values().addAll(Arrays.asList("A", "B", "C"));
assertMatchEquals("A", 1, model.getMatch(0));
assertMatchEquals("C", 3, model.getMatch(2));
}
@Test
public void matchViewListEvents() {
MatchModel<String, Integer> model = new MatchModel<String, Integer>();
ArrayList<String> copy = new ArrayList<String>();
GlazedLists.syncEventListToList(model.values(), copy);
model.addAll(Arrays.asList("A", "B", "C"), Arrays.asList(1, 2, 3, 4, 5));
assertArrayEquals(Arrays.asList("A", "B", "C").toArray(), copy.toArray());
}
private static <V, C> void assertMatchEquals(V expectedValue, C expectedCandidate, Match<V, C> actual) {
assertEquals(expectedValue, actual.getValue());
assertEquals(expectedCandidate, actual.getCandidate());
}
}