* added ScriptFormat and format configuration dialog

* added names view (did miss it last commit)
This commit is contained in:
Reinhard Pointner 2009-03-08 19:55:05 +00:00
parent deb15a6e15
commit 2de1b8a1b0
10 changed files with 878 additions and 57 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 723 B

View File

@ -0,0 +1,432 @@
package net.sourceforge.filebot.ui;
import static java.awt.Font.BOLD;
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;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.text.Format;
import java.text.ParseException;
import java.util.Arrays;
import java.util.ResourceBundle;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.script.ScriptException;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.BorderFactory;
import javax.swing.InputVerifier;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JDialog;
import javax.swing.JFormattedTextField;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.JFormattedTextField.AbstractFormatter;
import javax.swing.border.LineBorder;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.DefaultFormatterFactory;
import net.miginfocom.swing.MigLayout;
import net.sourceforge.filebot.ResourceManager;
import net.sourceforge.filebot.Settings;
import net.sourceforge.filebot.web.Episode;
import net.sourceforge.filebot.web.Episode.EpisodeFormat;
import net.sourceforge.tuned.ExceptionUtilities;
import net.sourceforge.tuned.ui.GradientStyle;
import net.sourceforge.tuned.ui.LinkButton;
import net.sourceforge.tuned.ui.TunedUtilities;
import net.sourceforge.tuned.ui.notification.SeparatorBorder;
import net.sourceforge.tuned.ui.notification.SeparatorBorder.Position;
public class EpisodeFormatDialog extends JDialog {
private Format selectedFormat = null;
protected JFormattedTextField preview = new JFormattedTextField(getPreviewSample());
protected JLabel errorMessage = new JLabel(ResourceManager.getIcon("dialog.cancel"));
protected JTextField editor = new JTextField();
protected Color defaultColor = preview.getForeground();
protected Color errorColor = Color.red;
public EpisodeFormatDialog(Window owner) {
super(owner, "Episode Format", ModalityType.DOCUMENT_MODAL);
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
editor.setFont(new Font(MONOSPACED, PLAIN, 14));
editor.setText(Settings.userRoot().get("dialog.format"));
preview.setBorder(BorderFactory.createEmptyBorder());
// update preview to current format
checkEpisodeFormat();
// bold title label in header
JLabel title = new JLabel(this.getTitle());
title.setFont(title.getFont().deriveFont(BOLD));
// status.setVisible(false);
JPanel header = new JPanel(new MigLayout("insets dialog, nogrid, fillx"));
header.setBackground(Color.white);
header.setBorder(new SeparatorBorder(1, new Color(0xB4B4B4), new Color(0xACACAC), GradientStyle.LEFT_TO_RIGHT, Position.BOTTOM));
header.add(title, "wrap unrel:push");
header.add(errorMessage, "gap indent, hidemode 3");
header.add(preview, "gap indent, hidemode 3, growx");
JPanel content = new JPanel(new MigLayout("insets dialog, nogrid, fill"));
content.add(editor, "wmin 120px, h 40px!, growx, wrap 8px");
content.add(new JLabel("Syntax"), "gap indent+unrel, wrap 0");
content.add(createSyntaxPanel(), "gapx indent indent, wrap 8px");
content.add(new JLabel("Examples"), "gap indent+unrel, wrap 0");
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(cancelAction), "tag cancel");
JComponent pane = (JComponent) getContentPane();
pane.setLayout(new MigLayout("insets 0, fill"));
pane.add(header, "h 60px, growx, dock north");
pane.add(content, "grow");
pack();
setLocation(TunedUtilities.getPreferredLocation(this));
TunedUtilities.putActionForKeystroke(pane, KeyStroke.getKeyStroke("released ESCAPE"), cancelAction);
// update format on change
editor.getDocument().addDocumentListener(new DocumentAdapter() {
@Override
public void update(DocumentEvent evt) {
checkEpisodeFormat();
}
});
// keep focus on preview, if current text doesn't fit episode format
preview.setInputVerifier(new InputVerifier() {
@Override
public boolean verify(JComponent input) {
return checkPreviewSample();
}
});
// check edit format on change
preview.getDocument().addDocumentListener(new DocumentAdapter() {
@Override
public void update(DocumentEvent evt) {
checkPreviewSample();
}
});
// focus editor by default
addWindowFocusListener(new WindowAdapter() {
@Override
public void windowGainedFocus(WindowEvent e) {
editor.requestFocusInWindow();
}
});
}
protected JPanel createSyntaxPanel() {
JPanel panel = new JPanel(new MigLayout("fill, nogrid"));
panel.setBorder(new LineBorder(new Color(0xACA899)));
panel.setBackground(new Color(0xFFFFE1));
panel.setOpaque(true);
panel.add(new JLabel(ResourceBundle.getBundle(getClass().getName()).getString("syntax")));
return panel;
}
protected JPanel createExamplesPanel() {
JPanel panel = new JPanel(new MigLayout("fill, wrap 3"));
panel.setBorder(new LineBorder(new Color(0xACA899)));
panel.setBackground(new Color(0xFFFFE1));
panel.setOpaque(true);
ResourceBundle bundle = ResourceBundle.getBundle(getClass().getName());
// sort keys
String[] keys = bundle.keySet().toArray(new String[0]);
Arrays.sort(keys);
for (String key : keys) {
if (key.startsWith("example")) {
String format = bundle.getString(key);
LinkButton formatLink = new LinkButton(new ExampleFormatAction(format));
formatLink.setFont(new Font(MONOSPACED, PLAIN, 11));
panel.add(formatLink);
panel.add(new JLabel("..."));
panel.add(new ExampleFormatLabel(format));
}
}
return panel;
}
protected Episode getPreviewSample() {
String sample = Settings.userRoot().get("dialog.sample");
if (sample != null) {
try {
return EpisodeFormat.getInstance().parseObject(sample);
} catch (Exception e) {
Logger.getLogger("global").log(Level.WARNING, e.getMessage(), e);
}
}
return new Episode("Dark Angel", "3", "1", "Labyrinth");
}
protected boolean checkPreviewSample() {
// check if field is being edited
if (preview.hasFocus()) {
try {
// try to parse text
preview.getFormatter().stringToValue(preview.getText());
} catch (Exception e) {
preview.setForeground(errorColor);
// failed to parse text
return false;
}
}
preview.setForeground(defaultColor);
return true;
}
protected DefaultFormatterFactory createFormatterFactory(Format display) {
DefaultFormatterFactory factory = new DefaultFormatterFactory();
factory.setEditFormatter(new SimpleFormatter(EpisodeFormat.getInstance()));
if (display != null) {
factory.setDisplayFormatter(new SimpleFormatter(display));
}
return factory;
}
protected boolean checkEpisodeFormat() {
Exception exception = null;
try {
Format format = new EpisodeScriptFormat(editor.getText().trim());
// check if format produces empty strings
if (format.format(preview.getValue()).trim().isEmpty()) {
throw new IllegalArgumentException("Format must not be empty.");
}
// update preview
preview.setFormatterFactory(createFormatterFactory(format));
} catch (Exception e) {
exception = e;
}
errorMessage.setText(exception != null ? ExceptionUtilities.getRootCauseMessage(exception) : null);
errorMessage.setVisible(exception != null);
preview.setVisible(exception == null);
editor.setForeground(exception == null ? defaultColor : errorColor);
return exception == null;
}
public Format getSelectedFormat() {
return selectedFormat;
}
private void finish(Format format) {
this.selectedFormat = format;
setVisible(false);
dispose();
if (checkEpisodeFormat()) {
Settings.userRoot().put("dialog.format", editor.getText());
}
if (checkPreviewSample()) {
Settings.userRoot().put("dialog.sample", preview.getValue().toString());
}
}
protected final Action cancelAction = new AbstractAction("Cancel", ResourceManager.getIcon("dialog.cancel")) {
@Override
public void actionPerformed(ActionEvent e) {
finish(null);
}
};
protected final Action useDefaultFormatAction = new AbstractAction("Default", ResourceManager.getIcon("dialog.default")) {
@Override
public void actionPerformed(ActionEvent e) {
finish(EpisodeFormat.getInstance());
}
};
protected final Action useCustomFormatAction = new AbstractAction("Use Format", ResourceManager.getIcon("dialog.continue")) {
@Override
public void actionPerformed(ActionEvent evt) {
try {
finish(new EpisodeScriptFormat(editor.getText()));
} catch (ScriptException e) {
Logger.getLogger("ui").log(Level.WARNING, ExceptionUtilities.getRootCauseMessage(e), e);
}
}
};
public static Format showDialog(Component parent) {
EpisodeFormatDialog dialog = new EpisodeFormatDialog(parent != null ? SwingUtilities.getWindowAncestor(parent) : null);
dialog.setVisible(true);
return dialog.getSelectedFormat();
}
protected class ExampleFormatAction extends AbstractAction {
public ExampleFormatAction(String format) {
super(format);
}
@Override
public void actionPerformed(ActionEvent e) {
editor.setText(getValue(Action.NAME).toString());
}
}
protected class ExampleFormatLabel extends JLabel {
private final String format;
public ExampleFormatLabel(String format) {
this.format = format;
// initialize text
updateText(preview.getValue());
// bind text to preview
preview.addPropertyChangeListener("value", new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
updateText(evt.getNewValue());
}
});
}
public void updateText(Object episode) {
try {
setText(new EpisodeScriptFormat(format).format(episode));
setForeground(defaultColor);
} catch (Exception e) {
setText(ExceptionUtilities.getRootCauseMessage(e));
setForeground(errorColor);
}
}
}
protected static class SimpleFormatter extends AbstractFormatter {
private final Format format;
public SimpleFormatter(Format format) {
this.format = format;
}
@Override
public String valueToString(Object value) throws ParseException {
return format.format(value);
}
@Override
public Object stringToValue(String text) throws ParseException {
return format.parseObject(text);
}
}
protected static class DocumentAdapter implements DocumentListener {
@Override
public void changedUpdate(DocumentEvent e) {
update(e);
}
@Override
public void insertUpdate(DocumentEvent e) {
update(e);
}
@Override
public void removeUpdate(DocumentEvent e) {
update(e);
}
public void update(DocumentEvent e) {
}
}
}

View File

@ -0,0 +1,13 @@
syntax: <html><b>{</b> <b>}</b> ... expression, <b>n</b> ... name, <b>s</b> ... season, <b>e</b> ... episode, <b>t</b> ... title</html>
# basic 1.01
example[0]: {n} - {s}.{e} - {t}
# 1x01
example[1]: {n} - {if (s) s+'x'}{e.pad(2)}
# S01E01
example[2]: {n} - {if (s) 'S'+s.pad(2)}E{e.pad(2)}
# uglyfy name
example[3]: {n.replace(/\\s+/g,'.').toLowerCase()}

View File

@ -0,0 +1,32 @@
package net.sourceforge.filebot.ui;
import javax.script.Bindings;
import javax.script.ScriptException;
import javax.script.SimpleBindings;
import net.sourceforge.filebot.web.Episode;
public class EpisodeScriptFormat extends ScriptFormat {
public EpisodeScriptFormat(String format) throws ScriptException {
super(format);
}
@Override
protected Bindings getBindings(Object object) {
Episode episode = (Episode) object;
Bindings bindings = new SimpleBindings();
bindings.put("n", episode.getSeriesName());
bindings.put("s", episode.getSeasonNumber());
bindings.put("e", episode.getEpisodeNumber());
bindings.put("t", episode.getTitle());
return bindings;
}
}

View File

@ -0,0 +1,16 @@
String.prototype.pad = function(length, padding) {
if (padding == undefined) {
padding = '0';
}
var s = this;
if (parseInt(this) >= 0 && padding.length >= 1) {
while (s.length < length) {
s = padding.concat(s)
}
}
return s;
};

View File

@ -0,0 +1,113 @@
package net.sourceforge.filebot.ui;
import java.io.InputStreamReader;
import java.text.FieldPosition;
import java.text.Format;
import java.text.ParsePosition;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.script.Bindings;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public abstract class ScriptFormat extends Format {
private final String format;
private final Object[] expressions;
public ScriptFormat(String format) throws ScriptException {
this.format = format;
this.expressions = compile(format, (Compilable) initScriptEngine());
}
protected ScriptEngine initScriptEngine() throws ScriptException {
ScriptEngine engine = new ScriptEngineManager().getEngineByName("JavaScript");
engine.eval(new InputStreamReader(getClass().getResourceAsStream("ScriptFormat.global.js")));
return engine;
}
public String getFormat() {
return format;
}
protected Object[] compile(String format, Compilable engine) throws ScriptException {
List<Object> expression = new ArrayList<Object>();
Matcher matcher = Pattern.compile("\\{([^\\{]*?)\\}").matcher(format);
int position = 0;
while (matcher.find()) {
if (position < matcher.start()) {
// literal before
expression.add(format.substring(position, matcher.start()));
}
String script = matcher.group(1);
if (script.length() > 0) {
// compiled script, or literal
expression.add(engine.compile(script));
}
position = matcher.end();
}
if (position < format.length()) {
// tail
expression.add(format.substring(position, format.length()));
}
return expression.toArray();
}
protected abstract Bindings getBindings(Object object);
@Override
public StringBuffer format(Object object, StringBuffer sb, FieldPosition pos) {
Bindings bindings = getBindings(object);
try {
for (Object snipped : expressions) {
if (snipped instanceof String) {
sb.append(snipped);
} else {
Object value = ((CompiledScript) snipped).eval(bindings);
if (value != null) {
sb.append(value);
}
}
}
} catch (ScriptException e) {
throw new IllegalArgumentException(e);
}
return sb;
}
@Override
public Object parseObject(String source, ParsePosition pos) {
throw new UnsupportedOperationException();
}
}

View File

@ -21,8 +21,6 @@ import java.util.Scanner;
import java.util.logging.Level;
import java.util.logging.Logger;
import ca.odell.glazedlists.EventList;
import net.sourceforge.filebot.torrent.Torrent;
import net.sourceforge.filebot.ui.transfer.FileTransferablePolicy;
import net.sourceforge.tuned.FileUtilities;
@ -30,10 +28,10 @@ import net.sourceforge.tuned.FileUtilities;
class NamesListTransferablePolicy extends FileTransferablePolicy {
private final EventList<Object> model;
private final List<Object> model;
public NamesListTransferablePolicy(EventList<Object> model) {
public NamesListTransferablePolicy(List<Object> model) {
this.model = model;
}

View File

@ -0,0 +1,134 @@
package net.sourceforge.filebot.ui.panel.rename;
import static net.sourceforge.filebot.FileBotUtilities.isInvalidFileName;
import java.awt.Component;
import java.util.AbstractList;
import java.util.ArrayList;
import java.util.List;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.TransformedList;
import ca.odell.glazedlists.event.ListEvent;
public class NamesViewEventList extends TransformedList<Object, String> {
private final List<String> names = new ArrayList<String>();
private final Component parent;
public NamesViewEventList(Component parent, EventList<Object> source) {
super(source);
this.parent = parent;
// connect to source list
source.addListEventListener(this);
}
@Override
protected boolean isWritable() {
return true;
}
@Override
public String get(int index) {
return names.get(index);
}
protected String format(Object object) {
return object.toString();
}
@Override
public void listChanged(ListEvent<Object> listChanges) {
EventList<Object> source = listChanges.getSourceList();
IndexView<String> newValues = new IndexView<String>(names);
while (listChanges.next()) {
int index = listChanges.getIndex();
int type = listChanges.getType();
switch (type) {
case ListEvent.INSERT:
names.add(index, format(source.get(index)));
newValues.getIndexFilter().add(index);
break;
case ListEvent.UPDATE:
names.set(index, format(source.get(index)));
newValues.getIndexFilter().add(index);
break;
case ListEvent.DELETE:
names.remove(index);
break;
}
}
submit(newValues);
listChanges.reset();
updates.forwardEvent(listChanges);
}
protected void submit(List<String> values) {
IndexView<String> invalidValues = new IndexView<String>(values);
for (int i = 0; i < values.size(); i++) {
if (isInvalidFileName(values.get(i))) {
invalidValues.getIndexFilter().add(i);
}
}
if (invalidValues.size() > 0) {
// validate names
ValidateNamesDialog.showDialog(parent, invalidValues);
}
}
protected static class IndexView<E> extends AbstractList<E> {
private final List<E> source;
private final List<Integer> indexFilter = new ArrayList<Integer>();
public IndexView(List<E> source) {
this.source = source;
}
public List<Integer> getIndexFilter() {
return indexFilter;
}
@Override
public E get(int index) {
return source.get(indexFilter.get(index));
}
@Override
public E set(int index, E element) {
return source.set(indexFilter.get(index), element);
};
@Override
public int size() {
return indexFilter.size();
}
}
}

View File

@ -3,7 +3,12 @@ package net.sourceforge.filebot.web;
import java.io.Serializable;
import java.text.NumberFormat;
import java.text.FieldPosition;
import java.text.Format;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class Episode implements Serializable {
@ -47,69 +52,78 @@ public class Episode implements Serializable {
}
public void setSeriesName(String seriesName) {
this.seriesName = seriesName;
}
public void setSeasonNumber(String seasonNumber) {
this.seasonNumber = seasonNumber;
}
public void setEpisodeNumber(String episodeNumber) {
this.episodeNumber = episodeNumber;
}
public void setTitle(String episodeName) {
this.title = episodeName;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder(40);
sb.append(seriesName).append(" - ");
if (seasonNumber != null) {
sb.append(seasonNumber).append("x");
}
sb.append(episodeNumber).append(" - ").append(title);
return sb.toString();
return EpisodeFormat.getInstance().format(this);
}
public static <T extends Iterable<Episode>> T formatEpisodeNumbers(T episodes, int minDigits) {
// find max. episode number length
for (Episode episode : episodes) {
try {
String episodeNumber = episode.getEpisodeNumber();
public static class EpisodeFormat extends Format {
if (episodeNumber.length() > minDigits && Integer.parseInt(episodeNumber) > 0) {
minDigits = episodeNumber.length();
private static final EpisodeFormat instance = new EpisodeFormat();
public static EpisodeFormat getInstance() {
return instance;
}
@Override
public StringBuffer format(Object obj, StringBuffer sb, FieldPosition pos) {
Episode episode = (Episode) obj;
sb.append(episode.getSeriesName()).append(" - ");
if (episode.getSeasonNumber() != null) {
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
}
} catch (NumberFormatException e) {
// ignore
}
return number;
}
// pad episode numbers with zeros (e.g. %02d) so all episode numbers have the same number of digits
NumberFormat numberFormat = NumberFormat.getIntegerInstance();
numberFormat.setMinimumIntegerDigits(minDigits);
numberFormat.setGroupingUsed(false);
for (Episode episode : episodes) {
try {
episode.setEpisodeNumber(numberFormat.format(Integer.parseInt(episode.getEpisodeNumber())));
} catch (NumberFormatException e) {
// ignore
@Override
public Episode parseObject(String source, ParsePosition pos) {
Pattern pattern = Pattern.compile("(.*) - (?:(\\w+?)x)?(\\w+)? - (.*)");
Matcher matcher = pattern.matcher(source).region(pos.getIndex(), source.length());
if (!matcher.matches()) {
pos.setErrorIndex(matcher.regionStart());
return null;
}
// episode number must not be null
if (matcher.group(3) == null) {
pos.setErrorIndex(matcher.start(3));
return null;
}
pos.setIndex(matcher.end());
return new Episode(matcher.group(1), matcher.group(2), matcher.group(3), matcher.group(4));
}
@Override
public Episode parseObject(String source) throws ParseException {
return (Episode) super.parseObject(source);
}
return episodes;
}
}

View File

@ -0,0 +1,69 @@
package net.sourceforge.filebot.ui;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import javax.script.Bindings;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.ScriptException;
import javax.script.SimpleBindings;
import org.junit.Test;
public class ScriptFormatTest {
@Test
public void compile() throws Exception {
ScriptFormat format = new TestScriptFormat("");
Object[] expression = format.compile("name: {name}, number: {number}", (Compilable) format.initScriptEngine());
assertEquals(4, expression.length, 0);
assertTrue(expression[0] instanceof String);
assertTrue(expression[1] instanceof CompiledScript);
assertTrue(expression[2] instanceof String);
assertTrue(expression[3] instanceof CompiledScript);
}
@Test
public void format() throws Exception {
assertEquals("X5-452", new TestScriptFormat("X5-{value}").format("452"));
// test pad
assertEquals("[007]", new TestScriptFormat("[{value.pad(3)}]").format("7"));
// choice
assertEquals("not to be", new TestScriptFormat("{if (value) 'to be'; else 'not to be'}").format(null));
// empty choice
assertEquals("", new TestScriptFormat("{if (value) 'to be'}").format(null));
// loop
assertEquals("0123456789", new TestScriptFormat("{var s=''; for (var i=0; i<parseInt(value);i++) s+=i;}").format("10"));
}
protected static class TestScriptFormat extends ScriptFormat {
public TestScriptFormat(String format) throws ScriptException {
super(format);
}
@Override
protected Bindings getBindings(Object value) {
Bindings bindings = new SimpleBindings();
bindings.put("value", value);
return bindings;
}
}
}