* 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:
parent
54bf7c2ca3
commit
d5a5b93b3b
|
@ -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();
|
||||
|
|
|
@ -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 |
|
@ -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);
|
||||
|
|
|
@ -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)) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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 {
|
||||
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue