diff --git a/source/net/sourceforge/filebot/format/BindingException.java b/source/net/sourceforge/filebot/format/BindingException.java index ee0b9d2c..bcf6a573 100644 --- a/source/net/sourceforge/filebot/format/BindingException.java +++ b/source/net/sourceforge/filebot/format/BindingException.java @@ -1,21 +1,17 @@ - package net.sourceforge.filebot.format; - public class BindingException extends RuntimeException { - + public BindingException(String message, Throwable cause) { super(message, cause); } - public BindingException(String binding, String innerMessage) { this(binding, innerMessage, null); } - public BindingException(String binding, String innerMessage, Throwable cause) { - this(String.format("BindingError: \"%s\": %s", binding, innerMessage), cause); + this(String.format("BindingException: \"%s\": %s", binding, innerMessage), cause); } - + } diff --git a/source/net/sourceforge/filebot/ui/rename/FormatDialog.java b/source/net/sourceforge/filebot/ui/rename/FormatDialog.java index 76df2f70..d30e0599 100644 --- a/source/net/sourceforge/filebot/ui/rename/FormatDialog.java +++ b/source/net/sourceforge/filebot/ui/rename/FormatDialog.java @@ -141,7 +141,7 @@ public class FormatDialog extends JDialog { } } - public FormatDialog(Window owner) { + public FormatDialog(Window owner, Mode initMode, MediaBindingBean lockOnBinding) { super(owner, ModalityType.DOCUMENT_MODAL); // initialize hidden @@ -223,26 +223,29 @@ public class FormatDialog extends JDialog { // install editor suggestions popup editor.setComponentPopupMenu(createRecentFormatPopup()); - // episode mode by default - setMode(Mode.Episode); - // initialize window properties setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); setSize(610, 430); + + // initialize data + setState(initMode, lockOnBinding != null ? lockOnBinding : restoreSample(initMode), lockOnBinding != null); } - public void setMode(Mode mode) { + public void setState(Mode mode, MediaBindingBean bindings, boolean locked) { this.mode = mode; - this.setTitle(String.format("%s Format", mode)); + this.setTitle(String.format(locked ? "%s Format - %s ⇔ %s" : "%s Format", mode, bindings.getInfoObject(), bindings.getMediaFile().getName())); title.setText(this.getTitle()); status.setVisible(false); switchEditModeAction.putValue(Action.NAME, String.format("Switch to %s Format", mode.next())); + switchEditModeAction.setEnabled(!locked); + changeSampleAction.setEnabled(!locked); + updateHelpPanel(mode); // update preview to current format - sample = restoreSample(mode); + sample = bindings; // restore editor state setFormatCode(mode.persistentFormatHistory().isEmpty() ? "" : mode.persistentFormatHistory().get(0)); @@ -388,7 +391,7 @@ public class FormatDialog extends JDialog { return panel; } - private MediaBindingBean restoreSample(Mode mode) { + protected MediaBindingBean restoreSample(Mode mode) { Object info = null; File media = null; @@ -646,7 +649,8 @@ public class FormatDialog extends JDialog { @Override public void actionPerformed(ActionEvent e) { - setMode(mode.next()); + Mode next = mode.next(); + setState(next, restoreSample(next), false); } }; diff --git a/source/net/sourceforge/filebot/ui/rename/RenameModel.java b/source/net/sourceforge/filebot/ui/rename/RenameModel.java index 86505bdd..2110a231 100644 --- a/source/net/sourceforge/filebot/ui/rename/RenameModel.java +++ b/source/net/sourceforge/filebot/ui/rename/RenameModel.java @@ -1,7 +1,5 @@ - package net.sourceforge.filebot.ui.rename; - import static net.sourceforge.tuned.FileUtilities.*; import java.beans.PropertyChangeEvent; @@ -31,67 +29,59 @@ import ca.odell.glazedlists.EventList; import ca.odell.glazedlists.TransformedList; import ca.odell.glazedlists.event.ListEvent; - public class RenameModel extends MatchModel { - + private final FormattedFutureEventList names = new FormattedFutureEventList(this.values()); - + private final Map formatters = new LinkedHashMap(); - + private final MatchFormatter defaultFormatter = new MatchFormatter() { - + @Override public boolean canFormat(Match match) { return true; } - - + @Override public String preview(Match match) { return format(match, null); } - - + @Override public String format(Match match, Map context) { // clean up path separators like / or \ return replacePathSeparators(String.valueOf(match.getValue())).trim(); } }; - + private boolean preserveExtension = true; - - + public EventList names() { return names; } - - + public EventList files() { return candidates(); } - - + public boolean preserveExtension() { return preserveExtension; } - - + public void setPreserveExtension(boolean preserveExtension) { this.preserveExtension = preserveExtension; } - - + public Map getRenameMap() { Map map = new LinkedHashMap(); - + for (int i = 0; i < names.size(); i++) { if (hasComplement(i)) { File originalFile = files().get(i); FormattedFuture formattedFuture = names.get(i); - + StringBuilder nameBuilder = new StringBuilder(); - + // append formatted name, throw exception if not ready try { nameBuilder.append(formattedFuture.get(0, TimeUnit.SECONDS)); @@ -102,116 +92,122 @@ public class RenameModel extends MatchModel { } catch (InterruptedException e) { throw new RuntimeException(e); } - + // append extension, if desired if (preserveExtension) { String extension = FileUtilities.getExtension(originalFile); - + if (extension != null) { nameBuilder.append('.').append(extension.toLowerCase()); } } - + // insert mapping if (map.put(originalFile, nameBuilder.toString()) != null) { throw new IllegalStateException(String.format("Duplicate file entry: \"%s\"", originalFile.getName())); } } } - + return map; } - - + public void useFormatter(Object key, MatchFormatter formatter) { if (formatter != null) { formatters.put(key, formatter); } else { formatters.remove(key); } - + // reformat matches names.refresh(); } - - + private MatchFormatter getFormatter(Match match) { for (MatchFormatter formatter : formatters.values()) { if (formatter.canFormat(match)) { return formatter; } } - + return defaultFormatter; } - - + + public Map getMatchContext() { + return new AbstractMap() { + + @Override + public Set> entrySet() { + Set> context = new LinkedHashSet>(); + for (Match it : matches()) { + if (it.getValue() != null && it.getCandidate() != null) { + context.add(new SimpleImmutableEntry(it.getCandidate(), it.getValue())); + } + } + return context; + } + }; + } + 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(EventList source) { super(source); this.source.addListEventListener(this); } - - + @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), getContext()); - + final FormattedFuture future = new FormattedFuture(match, getFormatter(match), getMatchContext()); + // update data if (type == ListEvent.INSERT) { futures.add(index, future); @@ -219,15 +215,15 @@ public class RenameModel extends MatchModel { } 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, @@ -238,7 +234,7 @@ public class RenameModel extends MatchModel { } }); } - + // observe and enqueue worker task submit(future); } else if (type == ListEvent.DELETE) { @@ -248,71 +244,51 @@ public class RenameModel extends MatchModel { 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()), getContext()); - + FormattedFuture future = new FormattedFuture(obsolete.getMatch(), getFormatter(obsolete.getMatch()), getMatchContext()); + // replace and cancel old future cancel(futures.set(i, future)); - + // submit new future submit(future); - + updates.elementUpdated(i, obsolete, future); } - + updates.commitEvent(); } - - - private Map getContext() { - return new AbstractMap() { - - @Override - public Set> entrySet() { - Set> context = new LinkedHashSet>(); - for (Match it : matches()) { - if (it.getValue() != null && it.getCandidate() != null) { - context.add(new SimpleImmutableEntry(it.getCandidate(), it.getValue())); - } - } - return context; - } - }; - } - - + 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() { - + @Override 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(); @@ -320,44 +296,37 @@ public class RenameModel extends MatchModel { } }; } - - + public static class FormattedFuture extends SwingWorker { - + private final Match match; private final Map context; - + private final MatchFormatter formatter; - - + private FormattedFuture(Match match, MatchFormatter formatter, Map context) { this.match = match; this.formatter = formatter; this.context = context; } - - + public boolean isComplexFormat() { return formatter instanceof ExpressionFormatter; } - - + public Match getMatch() { return match; } - - + public String preview() { return formatter.preview(match).trim(); } - - + @Override protected String doInBackground() throws Exception { return formatter.format(match, context).trim(); } - - + @Override public String toString() { if (isDone()) { @@ -367,10 +336,10 @@ public class RenameModel extends MatchModel { return String.format("[%s] %s", e instanceof ExecutionException ? e.getCause().getMessage() : e, preview()); } } - + // use preview if we are not ready yet return preview(); } } - + } diff --git a/source/net/sourceforge/filebot/ui/rename/RenamePanel.java b/source/net/sourceforge/filebot/ui/rename/RenamePanel.java index 38ca66d8..65ccbd23 100644 --- a/source/net/sourceforge/filebot/ui/rename/RenamePanel.java +++ b/source/net/sourceforge/filebot/ui/rename/RenamePanel.java @@ -23,6 +23,7 @@ import java.util.EnumSet; import java.util.LinkedList; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.logging.Level; import java.util.logging.Logger; @@ -50,8 +51,10 @@ import net.sourceforge.filebot.ResourceManager; import net.sourceforge.filebot.Settings; import net.sourceforge.filebot.StandardRenameAction; import net.sourceforge.filebot.WebServices; +import net.sourceforge.filebot.format.MediaBindingBean; import net.sourceforge.filebot.similarity.Match; import net.sourceforge.filebot.ui.Language; +import net.sourceforge.filebot.ui.rename.FormatDialog.Mode; import net.sourceforge.filebot.ui.rename.RenameModel.FormattedFuture; import net.sourceforge.filebot.web.AudioTrack; import net.sourceforge.filebot.web.AudioTrackFormat; @@ -85,6 +88,8 @@ public class RenamePanel extends JComponent { private static final PreferencesEntry persistentEpisodeFormat = Settings.forPackage(RenamePanel.class).entry("rename.format.episode"); private static final PreferencesEntry persistentMovieFormat = Settings.forPackage(RenamePanel.class).entry("rename.format.movie"); private static final PreferencesEntry persistentMusicFormat = Settings.forPackage(RenamePanel.class).entry("rename.format.music"); + + private static final PreferencesEntry persistentLastFormatState = Settings.forPackage(RenamePanel.class).entry("rename.last.format.state"); private static final PreferencesEntry persistentPreferredLanguage = Settings.forPackage(RenamePanel.class).entry("rename.language").defaultValue("en"); private static final PreferencesEntry persistentPreferredEpisodeOrder = Settings.forPackage(RenamePanel.class).entry("rename.episode.order").defaultValue("Airdate"); @@ -245,19 +250,15 @@ public class RenamePanel extends JComponent { @Override public void mouseClicked(MouseEvent evt) { if (evt.getClickCount() == 2) { + getWindow(evt.getSource()).setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); try { JList list = (JList) evt.getSource(); if (list.getSelectedIndex() >= 0) { - Object item = ((FormattedFuture) list.getSelectedValue()).getMatch().getValue(); - if (item instanceof Movie) { - getWindow(evt.getSource()).setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); - Movie m = (Movie) item; - if (m.getTmdbId() > 0) { - Desktop.getDesktop().browse(WebServices.TMDb.getMoviePageLink(m.getTmdbId())); - } else if (m.getImdbId() > 0) { - Desktop.getDesktop().browse(WebServices.IMDb.getMoviePageLink(m.getImdbId())); - } - } + Match match = renameModel.getMatch(list.getSelectedIndex()); + Map context = renameModel.getMatchContext(); + + MediaBindingBean sample = new MediaBindingBean(match.getValue(), match.getCandidate(), context); + showFormatEditor(sample); } } catch (Exception e) { Logger.getLogger(RenamePanel.class.getName()).log(Level.WARNING, e.getMessage()); @@ -312,26 +313,7 @@ public class RenamePanel extends JComponent { @Override public void actionPerformed(ActionEvent evt) { - FormatDialog dialog = new FormatDialog(getWindowAncestor(RenamePanel.this)); - dialog.setLocation(getOffsetLocation(dialog.getOwner())); - dialog.setVisible(true); - - if (dialog.submit()) { - switch (dialog.getMode()) { - case Episode: - renameModel.useFormatter(Episode.class, new ExpressionFormatter(dialog.getFormat().getExpression(), EpisodeFormat.SeasonEpisode, Episode.class)); - persistentEpisodeFormat.setValue(dialog.getFormat().getExpression()); - break; - case Movie: - renameModel.useFormatter(Movie.class, new ExpressionFormatter(dialog.getFormat().getExpression(), MovieFormat.NameYear, Movie.class)); - persistentMovieFormat.setValue(dialog.getFormat().getExpression()); - break; - case Music: - renameModel.useFormatter(AudioTrack.class, new ExpressionFormatter(dialog.getFormat().getExpression(), new AudioTrackFormat(), AudioTrack.class)); - persistentMusicFormat.setValue(dialog.getFormat().getExpression()); - break; - } - } + showFormatEditor(null); } }); @@ -408,6 +390,52 @@ public class RenamePanel extends JComponent { return actionPopup; } + protected void showFormatEditor(MediaBindingBean lockOnBinding) { + // default to Episode mode + Mode initMode = null; + + if (lockOnBinding == null || lockOnBinding.getInfoObject() instanceof Episode) { + initMode = Mode.Episode; + } else if (lockOnBinding.getInfoObject() instanceof Movie) { + initMode = Mode.Movie; + } else if (lockOnBinding.getInfoObject() instanceof AudioTrack) { + initMode = Mode.Music; + } + + // restore previous mode + if (lockOnBinding == null) { + try { + initMode = Mode.valueOf(persistentLastFormatState.getValue()); + } catch (Exception e) { + Logger.getLogger(RenamePanel.class.getName()).log(Level.WARNING, e.getMessage()); + } + } + + FormatDialog dialog = new FormatDialog(getWindowAncestor(RenamePanel.this), initMode, lockOnBinding); + dialog.setLocation(getOffsetLocation(dialog.getOwner())); + dialog.setVisible(true); + + if (dialog.submit()) { + switch (dialog.getMode()) { + case Episode: + renameModel.useFormatter(Episode.class, new ExpressionFormatter(dialog.getFormat().getExpression(), EpisodeFormat.SeasonEpisode, Episode.class)); + persistentEpisodeFormat.setValue(dialog.getFormat().getExpression()); + break; + case Movie: + renameModel.useFormatter(Movie.class, new ExpressionFormatter(dialog.getFormat().getExpression(), MovieFormat.NameYear, Movie.class)); + persistentMovieFormat.setValue(dialog.getFormat().getExpression()); + break; + case Music: + renameModel.useFormatter(AudioTrack.class, new ExpressionFormatter(dialog.getFormat().getExpression(), new AudioTrackFormat(), AudioTrack.class)); + persistentMusicFormat.setValue(dialog.getFormat().getExpression()); + break; + } + if (lockOnBinding == null) { + persistentLastFormatState.setValue(dialog.getMode().name()); + } + } + } + protected final Action clearFilesAction = new AbstractAction("Clear", ResourceManager.getIcon("action.clear")) { @Override