diff --git a/source/net/sourceforge/filebot/format/EpisodeFormatBindingBean.java b/source/net/sourceforge/filebot/format/EpisodeFormatBindingBean.java index 4af10b59..b19aed99 100644 --- a/source/net/sourceforge/filebot/format/EpisodeFormatBindingBean.java +++ b/source/net/sourceforge/filebot/format/EpisodeFormatBindingBean.java @@ -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(); diff --git a/source/net/sourceforge/filebot/format/ExpressionFormat.java b/source/net/sourceforge/filebot/format/ExpressionFormat.java index a2b679f8..4be3b198 100644 --- a/source/net/sourceforge/filebot/format/ExpressionFormat.java +++ b/source/net/sourceforge/filebot/format/ExpressionFormat.java @@ -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 expression = new ArrayList(); + protected Object[] compile(String expression, Compilable engine) throws ScriptException { + List compilation = new ArrayList(); - 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); diff --git a/source/net/sourceforge/filebot/resources/worker.pending.png b/source/net/sourceforge/filebot/resources/worker.pending.png new file mode 100644 index 00000000..57b03ce7 Binary files /dev/null and b/source/net/sourceforge/filebot/resources/worker.pending.png differ diff --git a/source/net/sourceforge/filebot/resources/worker.started.png b/source/net/sourceforge/filebot/resources/worker.started.png new file mode 100644 index 00000000..b2d3a98b Binary files /dev/null and b/source/net/sourceforge/filebot/resources/worker.started.png differ diff --git a/source/net/sourceforge/filebot/similarity/Match.java b/source/net/sourceforge/filebot/similarity/Match.java index 25f4ed37..4707d3e4 100644 --- a/source/net/sourceforge/filebot/similarity/Match.java +++ b/source/net/sourceforge/filebot/similarity/Match.java @@ -2,24 +2,24 @@ package net.sourceforge.filebot.similarity; -public class Match { +public class Match { - 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 { } + @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); diff --git a/source/net/sourceforge/filebot/similarity/Matcher.java b/source/net/sourceforge/filebot/similarity/Matcher.java index eb34d639..e6afb3c4 100644 --- a/source/net/sourceforge/filebot/similarity/Matcher.java +++ b/source/net/sourceforge/filebot/similarity/Matcher.java @@ -172,19 +172,12 @@ public class Matcher { protected static class DisjointMatchCollection extends AbstractList> { - private final List> matches; + private final List> matches = new ArrayList>(); - private final Map> values; - private final Map> candidates; + private final Map> values = new IdentityHashMap>(); + private final Map> candidates = new IdentityHashMap>(); - public DisjointMatchCollection() { - matches = new ArrayList>(); - values = new IdentityHashMap>(); - candidates = new IdentityHashMap>(); - } - - @Override public boolean add(Match match) { if (disjoint(match)) { diff --git a/source/net/sourceforge/filebot/torrent/BDecoder.java b/source/net/sourceforge/filebot/torrent/BDecoder.java index 294e2e9f..cac4dba9 100644 --- a/source/net/sourceforge/filebot/torrent/BDecoder.java +++ b/source/net/sourceforge/filebot/torrent/BDecoder.java @@ -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; diff --git a/source/net/sourceforge/filebot/torrent/Torrent.java b/source/net/sourceforge/filebot/torrent/Torrent.java index ad7939d9..9bb2e6fb 100644 --- a/source/net/sourceforge/filebot/torrent/Torrent.java +++ b/source/net/sourceforge/filebot/torrent/Torrent.java @@ -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(); } } diff --git a/source/net/sourceforge/filebot/ui/EpisodeFormatDialog.java b/source/net/sourceforge/filebot/ui/EpisodeFormatDialog.java index 09338d51..01ab84b6 100644 --- a/source/net/sourceforge/filebot/ui/EpisodeFormatDialog.java +++ b/source/net/sourceforge/filebot/ui/EpisodeFormatDialog.java @@ -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() { - - 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() { - // 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 { diff --git a/source/net/sourceforge/filebot/ui/panel/rename/EpisodeExpressionFormatter.java b/source/net/sourceforge/filebot/ui/panel/rename/EpisodeExpressionFormatter.java new file mode 100644 index 00000000..61d2a209 --- /dev/null +++ b/source/net/sourceforge/filebot/ui/panel/rename/EpisodeExpressionFormatter.java @@ -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)); + } + +} diff --git a/source/net/sourceforge/filebot/ui/panel/rename/FileNameFormatter.java b/source/net/sourceforge/filebot/ui/panel/rename/FileNameFormatter.java new file mode 100644 index 00000000..9ed7cb43 --- /dev/null +++ b/source/net/sourceforge/filebot/ui/panel/rename/FileNameFormatter.java @@ -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); + } + +} diff --git a/source/net/sourceforge/filebot/ui/panel/rename/FilesListTransferablePolicy.java b/source/net/sourceforge/filebot/ui/panel/rename/FilesListTransferablePolicy.java index b00f74f4..3db771dc 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/FilesListTransferablePolicy.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/FilesListTransferablePolicy.java @@ -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 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 files) { - for (File file : files) { - model.add(new FastFile(file.getPath())); + model.addAll(FastFile.foreach(files)); } } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/MatchAction.java b/source/net/sourceforge/filebot/ui/panel/rename/MatchAction.java index 2798d68f..8b9f0897 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/MatchAction.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/MatchAction.java @@ -40,12 +40,12 @@ import net.sourceforge.tuned.ui.ProgressDialog.Cancellable; class MatchAction extends AbstractAction { - private final RenameModel model; + private final RenameModel model; private final Collection metrics; - public MatchAction(RenameModel 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 matcher; - public BackgroundMatcher(RenameModel model, Collection metrics) { + public BackgroundMatcher(MatchModel model, Collection metrics) { // match names against files - this.matcher = new Matcher(model.names(), model.files(), metrics); + this.matcher = new Matcher(model.values(), model.candidates(), metrics); } @@ -215,14 +211,10 @@ class MatchAction extends AbstractAction { model.clear(); // put new data into model - for (Match 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); } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/MatchFormatter.java b/source/net/sourceforge/filebot/ui/panel/rename/MatchFormatter.java new file mode 100644 index 00000000..52d4d18b --- /dev/null +++ b/source/net/sourceforge/filebot/ui/panel/rename/MatchFormatter.java @@ -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); + +} diff --git a/source/net/sourceforge/filebot/ui/panel/rename/MatchModel.java b/source/net/sourceforge/filebot/ui/panel/rename/MatchModel.java new file mode 100644 index 00000000..bf86a1d5 --- /dev/null +++ b/source/net/sourceforge/filebot/ui/panel/rename/MatchModel.java @@ -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 { + + private final EventList> source = new BasicEventList>(); + + private final EventList values; + + private final EventList candidates; + + + public MatchModel() { + this.values = new MatchView(source) { + + @Override + public Value getElement(Match match) { + return match.getValue(); + } + + + @Override + public Candidate getComplement(Match match) { + return match.getCandidate(); + } + + + @Override + public Match createMatch(Value element, Candidate complement) { + return new Match(element, complement); + } + }; + + this.candidates = new MatchView(source) { + + @Override + public Candidate getElement(Match match) { + return match.getCandidate(); + } + + + @Override + public Value getComplement(Match match) { + return match.getValue(); + } + + + @Override + public Match createMatch(Candidate element, Value complement) { + return new Match(complement, element); + } + }; + } + + + public void clear() { + source.clear(); + } + + + public int size() { + return source.size(); + } + + + public Match 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> matches() { + return source; + } + + + public EventList values() { + return values; + } + + + public EventList candidates() { + return candidates; + } + + + public void addAll(Collection> matches) { + source.addAll(matches); + } + + + public void addAll(Collection values, Collection candidates) { + if (this.values.size() != this.candidates.size()) + throw new IllegalStateException("Existing matches are not balanced"); + + Iterator valueIterator = values.iterator(); + Iterator 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)); + } + } + + + private abstract class MatchView extends TransformedList, Element> { + + public MatchView(EventList> source) { + super(source); + + source.addListEventListener(this); + } + + + public abstract Element getElement(Match match); + + + public abstract Complement getComplement(Match match); + + + public abstract Match 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 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 range = new ArrayList(); + + 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(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 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> 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(); + } + } + +} diff --git a/source/net/sourceforge/filebot/ui/panel/rename/NamesListTransferablePolicy.java b/source/net/sourceforge/filebot/ui/panel/rename/NamesListTransferablePolicy.java index 3043442a..a8050211 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/NamesListTransferablePolicy.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/NamesListTransferablePolicy.java @@ -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); diff --git a/source/net/sourceforge/filebot/ui/panel/rename/NamesViewEventList.java b/source/net/sourceforge/filebot/ui/panel/rename/NamesViewEventList.java deleted file mode 100644 index f442636a..00000000 --- a/source/net/sourceforge/filebot/ui/panel/rename/NamesViewEventList.java +++ /dev/null @@ -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 { - - private final List names = new ArrayList(); - - private final Map, Format> formatMap = new HashMap, Format>(); - - private final Component parent; - - - public NamesViewEventList(Component parent, EventList 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 changes = new ArrayList(); - - // 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(names, changes)); - - updates.commitEvent(); - } - - - private String format(Object object) { - for (Entry, Format> entry : formatMap.entrySet()) { - if (entry.getKey().isInstance(object)) { - return entry.getValue().format(object); - } - } - - return object.toString(); - } - - - @Override - public void listChanged(ListEvent listChanges) { - EventList source = listChanges.getSourceList(); - List changes = new ArrayList(); - - 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(names, changes)); - - listChanges.reset(); - updates.forwardEvent(listChanges); - } - - - protected void submit(List values) { - List issues = new ArrayList(); - - 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(values, issues)); - } - } - - - protected static class IndexView extends AbstractList { - - private final List source; - - private final List filter; - - - public IndexView(List source, List 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(); - } - - } - -} diff --git a/source/net/sourceforge/filebot/ui/panel/rename/RenameAction.java b/source/net/sourceforge/filebot/ui/panel/rename/RenameAction.java index 641f0014..f1d4cf99 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/RenameAction.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/RenameAction.java @@ -19,10 +19,10 @@ import net.sourceforge.tuned.FileUtilities; class RenameAction extends AbstractAction { - private final RenameModel model; + private final RenameModel model; - public RenameAction(RenameModel 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> todoQueue = new ArrayDeque>(); Deque> doneQueue = new ArrayDeque>(); - for (Match match : model.matches()) { + for (Match match : model.getMatchesForRenaming()) { File source = match.getCandidate(); String extension = FileUtilities.getExtension(source); diff --git a/source/net/sourceforge/filebot/ui/panel/rename/RenameList.java b/source/net/sourceforge/filebot/ui/panel/rename/RenameList.java index 7ae88eea..dc916438 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/RenameList.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/RenameList.java @@ -59,14 +59,13 @@ class RenameList extends FileBotList { } - 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 extends FileBotList { 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 extends FileBotList { 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 extends FileBotList { 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; + } } }; diff --git a/source/net/sourceforge/filebot/ui/panel/rename/RenameListCellRenderer.java b/source/net/sourceforge/filebot/ui/panel/rename/RenameListCellRenderer.java index 702629b8..2556b2e1 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/RenameListCellRenderer.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/RenameListCellRenderer.java @@ -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 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; } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/RenameModel.java b/source/net/sourceforge/filebot/ui/panel/rename/RenameModel.java index 2ab0b07f..68000310 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/RenameModel.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/RenameModel.java @@ -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 { +public class RenameModel extends MatchModel { - private final EventList names; - private final EventList files; + private final FormattedFutureEventList names = new FormattedFutureEventList(); + + private final Map, MatchFormatter> formatters = new HashMap, 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 names, EventList 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 names() { + public EventList names() { return names; } - public EventList files() { - return files; + public EventList files() { + return candidates(); } - public void clear() { - names.clear(); - files.clear(); - } - - - public int matchCount() { - return Math.min(names.size(), files.size()); - } - - - public Match getMatch(int index) { - if (index >= matchCount()) - throw new IndexOutOfBoundsException(); + public List> getMatchesForRenaming() { + List> matches = new ArrayList>(); - return new Match(names.get(index), files.get(index)); + for (int i = 0; i < size(); i++) { + if (hasComplement(i) && names.get(i).isDone()) { + matches.add(new Match(names().get(i).toString(), files().get(i))); + } + } + + return matches; } - public Collection> matches() { - return new AbstractList>() { - - @Override - public Match get(int index) { - return getMatch(index); + private MatchFormatter getFormatter(Match match) { + for (MatchFormatter formatter : formatters.values()) { + if (formatter.canFormat(match)) { + return formatter; } + } + + return defaultFormatter; + } + + + private class FormattedFutureEventList extends TransformedList { + + private final List futures = new ArrayList(); + + private final Executor backgroundFormatter = new ThreadPoolExecutor(0, 1, 5L, TimeUnit.SECONDS, new LinkedBlockingQueue()); + + + 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 listChanges) { + updates.beginEvent(true); + + while (listChanges.next()) { + int index = listChanges.getIndex(); + int type = listChanges.getType(); + + if (type == ListEvent.INSERT || type == ListEvent.UPDATE) { + Match 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 RenameModel create() { - return new RenameModel((EventList) new BasicEventList(), (EventList) new BasicEventList()); - } - + public static class FormattedFuture extends SwingWorker { + + private final Match match; + + private final MatchFormatter formatter; + + private String display; + + + private FormattedFuture(Match match, MatchFormatter formatter) { + this.match = match; + this.formatter = formatter; + + // initial display value + this.display = formatter.preview(match); + } + - public static RenameModel wrap(EventList names, EventList values) { - return new RenameModel(names, values); + public Match 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; + } } } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/RenamePanel.java b/source/net/sourceforge/filebot/ui/panel/rename/RenamePanel.java index 79640e6b..e1544976 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/RenamePanel.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/RenamePanel.java @@ -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 model = RenameModel.create(); + protected final RenameModel renameModel = new RenameModel(); - protected final NamesViewEventList namesView = new NamesViewEventList(this, model.names()); + protected final RenameList namesList = new RenameList(renameModel.names()); - protected final RenameList namesList = new RenameList(namesView); + protected final RenameList filesList = new RenameList(renameModel.files()); - protected final RenameList filesList = new RenameList(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 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()); - model.files().addListEventListener(new RepaintHandler()); + renameModel.names().addListEventListener(new RepaintHandler()); + renameModel.files().addListEventListener(new RepaintHandler()); } @@ -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 episodes = new ArrayList(); - List files = new ArrayList(); + List> matches = new ArrayList>(); for (Match match : get()) { - episodes.add(match.getCandidate()); - files.add(match.getValue()); + matches.add(new Match(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 persistentFormatExpression = Settings.userRoot().entry("rename.format", new AbstractAdapter() { + + @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()); + } + }); + } diff --git a/source/net/sourceforge/filebot/web/Episode.java b/source/net/sourceforge/filebot/web/Episode.java index 254dec9f..a8922475 100644 --- a/source/net/sourceforge/filebot/web/Episode.java +++ b/source/net/sourceforge/filebot/web/Episode.java @@ -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()); } diff --git a/source/net/sourceforge/tuned/FastFile.java b/source/net/sourceforge/tuned/FastFile.java index 1d806a39..9a5d8995 100644 --- a/source/net/sourceforge/tuned/FastFile.java +++ b/source/net/sourceforge/tuned/FastFile.java @@ -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 foreach(File... files) { + return foreach(Arrays.asList(files)); + } + + + public static List foreach(final List files) { + List result = new ArrayList(files.size()); + + for (File file : files) { + result.add(new FastFile(file.getPath())); + } + + return result; + } } diff --git a/source/net/sourceforge/tuned/FileUtilities.java b/source/net/sourceforge/tuned/FileUtilities.java index b76e293f..00092c71 100644 --- a/source/net/sourceforge/tuned/FileUtilities.java +++ b/source/net/sourceforge/tuned/FileUtilities.java @@ -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. diff --git a/test/net/sourceforge/filebot/FileBotTestSuite.java b/test/net/sourceforge/filebot/FileBotTestSuite.java index a8130fdf..eae1d849 100644 --- a/test/net/sourceforge/filebot/FileBotTestSuite.java +++ b/test/net/sourceforge/filebot/FileBotTestSuite.java @@ -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 { } diff --git a/test/net/sourceforge/filebot/MiscSuite.java b/test/net/sourceforge/filebot/MiscSuite.java deleted file mode 100644 index 13071d23..00000000 --- a/test/net/sourceforge/filebot/MiscSuite.java +++ /dev/null @@ -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 { - -} diff --git a/test/net/sourceforge/filebot/ui/panel/rename/MatchModelTest.java b/test/net/sourceforge/filebot/ui/panel/rename/MatchModelTest.java new file mode 100644 index 00000000..c2425b5c --- /dev/null +++ b/test/net/sourceforge/filebot/ui/panel/rename/MatchModelTest.java @@ -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 model = new MatchModel(); + + List names = Arrays.asList("A", "B", "C", "D", "E"); + List 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 model = new MatchModel(); + 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 model = new MatchModel(); + + 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 model = new MatchModel(); + + ArrayList copy = new ArrayList(); + 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 void assertMatchEquals(V expectedValue, C expectedCandidate, Match actual) { + assertEquals(expectedValue, actual.getValue()); + assertEquals(expectedCandidate, actual.getCandidate()); + } +}