1
0
mirror of https://github.com/mitb-archive/filebot synced 2024-08-13 17:03:45 -04:00

+ usability enhancements regarding FormatEditor

This commit is contained in:
Reinhard Pointner 2013-12-18 05:53:59 +00:00
parent f81e2fa9ea
commit 0d6ae94ae9
4 changed files with 161 additions and 164 deletions

View File

@ -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);
}
}

View File

@ -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);
}
};

View File

@ -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<Object, File> {
private final FormattedFutureEventList names = new FormattedFutureEventList(this.values());
private final Map<Object, MatchFormatter> formatters = new LinkedHashMap<Object, MatchFormatter>();
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<FormattedFuture> names() {
return names;
}
public EventList<File> files() {
return candidates();
}
public boolean preserveExtension() {
return preserveExtension;
}
public void setPreserveExtension(boolean preserveExtension) {
this.preserveExtension = preserveExtension;
}
public Map<File, String> getRenameMap() {
Map<File, String> map = new LinkedHashMap<File, String>();
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<Object, File> {
} 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<Object, File> match) {
for (MatchFormatter formatter : formatters.values()) {
if (formatter.canFormat(match)) {
return formatter;
}
}
return defaultFormatter;
}
public Map<File, Object> getMatchContext() {
return new AbstractMap<File, Object>() {
@Override
public Set<Entry<File, Object>> entrySet() {
Set<Entry<File, Object>> context = new LinkedHashSet<Entry<File, Object>>();
for (Match<Object, File> it : matches()) {
if (it.getValue() != null && it.getCandidate() != null) {
context.add(new SimpleImmutableEntry<File, Object>(it.getCandidate(), it.getValue()));
}
}
return context;
}
};
}
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(EventList<Object> 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<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), 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<Object, File> {
} 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<Object, File> {
}
});
}
// observe and enqueue worker task
submit(future);
} else if (type == ListEvent.DELETE) {
@ -248,71 +244,51 @@ public class RenameModel extends MatchModel<Object, File> {
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<File, Object> getContext() {
return new AbstractMap<File, Object>() {
@Override
public Set<Entry<File, Object>> entrySet() {
Set<Entry<File, Object>> context = new LinkedHashSet<Entry<File, Object>>();
for (Match<Object, File> it : matches()) {
if (it.getValue() != null && it.getCandidate() != null) {
context.add(new SimpleImmutableEntry<File, Object>(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<Object, File> {
}
};
}
public static class FormattedFuture extends SwingWorker<String, Void> {
private final Match<Object, File> match;
private final Map<File, Object> context;
private final MatchFormatter formatter;
private FormattedFuture(Match<Object, File> match, MatchFormatter formatter, Map<File, Object> context) {
this.match = match;
this.formatter = formatter;
this.context = context;
}
public boolean isComplexFormat() {
return formatter instanceof ExpressionFormatter;
}
public Match<Object, File> 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<Object, File> {
return String.format("[%s] %s", e instanceof ExecutionException ? e.getCause().getMessage() : e, preview());
}
}
// use preview if we are not ready yet
return preview();
}
}
}

View File

@ -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<String> persistentEpisodeFormat = Settings.forPackage(RenamePanel.class).entry("rename.format.episode");
private static final PreferencesEntry<String> persistentMovieFormat = Settings.forPackage(RenamePanel.class).entry("rename.format.movie");
private static final PreferencesEntry<String> persistentMusicFormat = Settings.forPackage(RenamePanel.class).entry("rename.format.music");
private static final PreferencesEntry<String> persistentLastFormatState = Settings.forPackage(RenamePanel.class).entry("rename.last.format.state");
private static final PreferencesEntry<String> persistentPreferredLanguage = Settings.forPackage(RenamePanel.class).entry("rename.language").defaultValue("en");
private static final PreferencesEntry<String> 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<Object, File> match = renameModel.getMatch(list.getSelectedIndex());
Map<File, Object> 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