diff --git a/source/net/sourceforge/filebot/format/ExpressionFormat.global.js b/source/net/sourceforge/filebot/format/ExpressionFormat.global.js index d1d76593..92abc73f 100644 --- a/source/net/sourceforge/filebot/format/ExpressionFormat.global.js +++ b/source/net/sourceforge/filebot/format/ExpressionFormat.global.js @@ -31,7 +31,11 @@ String.prototype.space = function(replacement) { * Remove trailing parenthesis including any leading whitespace. * * e.g. "Doctor Who (2005)" -> "Doctor Who" + * "Bad Wolf (1)" -> "Bad Wolf, Part 1" */ -String.prototype.removeTrailingBraces = function() { - return this.replace(/\s*\([^\)]*\)$/, ""); +String.prototype.replaceTrailingBraces = function(replacement) { + // use empty string as default replacement + var r = replacement ? replacement : ""; + + return this.replace(/\s*\(([^\)]*)\)$/, r); } diff --git a/source/net/sourceforge/filebot/format/ExpressionFormat.java b/source/net/sourceforge/filebot/format/ExpressionFormat.java index a6fbe3f2..00c320cc 100644 --- a/source/net/sourceforge/filebot/format/ExpressionFormat.java +++ b/source/net/sourceforge/filebot/format/ExpressionFormat.java @@ -117,7 +117,7 @@ public class ExpressionFormat extends Format { ScriptContext context = new SimpleScriptContext(); // use privileged bindings so we are not restricted by the script sandbox - context.setBindings(PrivilegedBindings.newProxy(bindings), ScriptContext.GLOBAL_SCOPE); + context.setBindings(PrivilegedBindings.newProxy(bindings, AccessController.getContext()), ScriptContext.GLOBAL_SCOPE); for (Object snipped : compilation) { if (snipped instanceof CompiledScript) { @@ -161,7 +161,7 @@ public class ExpressionFormat extends Format { Object snipped = compilation[i]; if (snipped instanceof CompiledScript) { - compilation[i] = new SecureCompiledScript(sandbox, (CompiledScript) snipped); + compilation[i] = new SecureCompiledScript((CompiledScript) snipped, sandbox); } } @@ -184,10 +184,12 @@ public class ExpressionFormat extends Format { private static class PrivilegedBindings implements InvocationHandler { private final Bindings bindings; + private final AccessControlContext context; - private PrivilegedBindings(Bindings bindings) { + private PrivilegedBindings(Bindings bindings, AccessControlContext context) { this.bindings = bindings; + this.context = context; } @@ -200,7 +202,7 @@ public class ExpressionFormat extends Format { public Object run() throws Exception { return method.invoke(bindings, args); } - }); + }, context); } catch (PrivilegedActionException e) { Throwable cause = e.getException(); @@ -216,8 +218,8 @@ public class ExpressionFormat extends Format { } - public static Bindings newProxy(Bindings bindings) { - PrivilegedBindings invocationHandler = new PrivilegedBindings(bindings); + public static Bindings newProxy(Bindings bindings, AccessControlContext context) { + InvocationHandler invocationHandler = new PrivilegedBindings(bindings, context); // create dynamic invocation proxy return (Bindings) Proxy.newProxyInstance(PrivilegedBindings.class.getClassLoader(), new Class[] { Bindings.class }, invocationHandler); @@ -227,13 +229,13 @@ public class ExpressionFormat extends Format { private static class SecureCompiledScript extends CompiledScript { - private final AccessControlContext sandbox; private final CompiledScript compiledScript; + private final AccessControlContext sandbox; - private SecureCompiledScript(AccessControlContext sandbox, CompiledScript compiledScript) { - this.sandbox = sandbox; + private SecureCompiledScript(CompiledScript compiledScript, AccessControlContext sandbox) { this.compiledScript = compiledScript; + this.sandbox = sandbox; } diff --git a/source/net/sourceforge/filebot/ui/AbstractSearchPanel.java b/source/net/sourceforge/filebot/ui/AbstractSearchPanel.java index 48699243..383099bb 100644 --- a/source/net/sourceforge/filebot/ui/AbstractSearchPanel.java +++ b/source/net/sourceforge/filebot/ui/AbstractSearchPanel.java @@ -96,7 +96,7 @@ public abstract class AbstractSearchPanel extends JComponent { AutoCompleteSupport.install(searchTextField.getEditor(), searchHistory).setFilterMode(TextMatcherEditor.CONTAINS); - TunedUtilities.putActionForKeystroke(this, KeyStroke.getKeyStroke("ENTER"), searchAction); + TunedUtilities.installAction(this, KeyStroke.getKeyStroke("ENTER"), searchAction); } diff --git a/source/net/sourceforge/filebot/ui/EpisodeFormatDialog.java b/source/net/sourceforge/filebot/ui/EpisodeFormatDialog.java index 8cb60b0f..8487c294 100644 --- a/source/net/sourceforge/filebot/ui/EpisodeFormatDialog.java +++ b/source/net/sourceforge/filebot/ui/EpisodeFormatDialog.java @@ -10,7 +10,6 @@ import java.awt.Color; import java.awt.Font; import java.awt.Window; import java.awt.event.ActionEvent; -import java.awt.event.ActionListener; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.beans.PropertyChangeEvent; @@ -45,8 +44,8 @@ import javax.swing.SwingWorker; import javax.swing.Timer; import javax.swing.border.LineBorder; import javax.swing.event.DocumentEvent; -import javax.swing.event.DocumentListener; import javax.swing.filechooser.FileNameExtensionFilter; +import javax.swing.undo.UndoManager; import net.miginfocom.swing.MigLayout; import net.sourceforge.filebot.ResourceManager; @@ -58,6 +57,7 @@ import net.sourceforge.filebot.web.EpisodeFormat; import net.sourceforge.tuned.DefaultThreadFactory; import net.sourceforge.tuned.ExceptionUtilities; import net.sourceforge.tuned.ui.GradientStyle; +import net.sourceforge.tuned.ui.LazyDocumentListener; import net.sourceforge.tuned.ui.LinkButton; import net.sourceforge.tuned.ui.ProgressIndicator; import net.sourceforge.tuned.ui.TunedUtilities; @@ -136,11 +136,18 @@ public class EpisodeFormatDialog extends JDialog { header.setComponentPopupMenu(createPreviewSamplePopup()); + // setup undo support + final UndoManager undo = new UndoManager(); + editor.getDocument().addUndoableEditListener(undo); + + // enable undo/redo + TunedUtilities.installUndoSupport(editor); + // update format on change - editor.getDocument().addDocumentListener(new LazyDocumentAdapter() { + editor.getDocument().addDocumentListener(new LazyDocumentListener() { @Override - public void update() { + public void update(DocumentEvent e) { checkFormatInBackground(); } }); @@ -207,7 +214,7 @@ public class EpisodeFormatDialog extends JDialog { File mediaFile = fileChooser.getSelectedFile(); try { - MediaInfoComponent.showMessageDialog(EpisodeFormatDialog.this, mediaFile); + MediaInfoPane.showMessageDialog(EpisodeFormatDialog.this, mediaFile); } catch (LinkageError e) { // MediaInfo native library is missing -> notify user Logger.getLogger("ui").log(Level.SEVERE, e.getMessage(), e); @@ -490,43 +497,4 @@ public class EpisodeFormatDialog extends JDialog { firePropertyChange("previewSample", null, previewSample); } - - protected static abstract class LazyDocumentAdapter implements DocumentListener { - - private final Timer timer = new Timer(200, new ActionListener() { - - @Override - public void actionPerformed(ActionEvent e) { - update(); - } - }); - - - public LazyDocumentAdapter() { - timer.setRepeats(false); - } - - - @Override - public void changedUpdate(DocumentEvent e) { - timer.restart(); - } - - - @Override - public void insertUpdate(DocumentEvent e) { - timer.restart(); - } - - - @Override - public void removeUpdate(DocumentEvent e) { - timer.restart(); - } - - - public abstract void update(); - - } - } diff --git a/source/net/sourceforge/filebot/ui/FileBotList.java b/source/net/sourceforge/filebot/ui/FileBotList.java index 90a1ed1b..345edd39 100644 --- a/source/net/sourceforge/filebot/ui/FileBotList.java +++ b/source/net/sourceforge/filebot/ui/FileBotList.java @@ -50,7 +50,7 @@ public class FileBotList extends JComponent { // Shortcut DELETE, disabled by default removeAction.setEnabled(false); - TunedUtilities.putActionForKeystroke(this, KeyStroke.getKeyStroke("pressed DELETE"), removeAction); + TunedUtilities.installAction(this, KeyStroke.getKeyStroke("pressed DELETE"), removeAction); } diff --git a/source/net/sourceforge/filebot/ui/MediaInfoComponent.java b/source/net/sourceforge/filebot/ui/MediaInfoPane.java similarity index 69% rename from source/net/sourceforge/filebot/ui/MediaInfoComponent.java rename to source/net/sourceforge/filebot/ui/MediaInfoPane.java index 7f2d57aa..5e43870e 100644 --- a/source/net/sourceforge/filebot/ui/MediaInfoComponent.java +++ b/source/net/sourceforge/filebot/ui/MediaInfoPane.java @@ -12,6 +12,7 @@ import java.util.Map; import java.util.Map.Entry; import javax.swing.AbstractAction; +import javax.swing.Action; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JDialog; @@ -26,61 +27,62 @@ import net.sourceforge.filebot.mediainfo.MediaInfo.StreamKind; import net.sourceforge.tuned.ui.TunedUtilities; -public class MediaInfoComponent extends JTabbedPane { +public class MediaInfoPane extends JTabbedPane { - public MediaInfoComponent(Map>> mediaInfo) { - insert(mediaInfo); + public MediaInfoPane(File file) { + // get media info + MediaInfo mediaInfo = new MediaInfo(); + + if (!mediaInfo.open(file)) + throw new IllegalArgumentException("Cannot open file: " + file); + + // create tab for each stream + for (Entry>> entry : mediaInfo.snapshot().entrySet()) { + for (Map parameters : entry.getValue()) { + addTableTab(entry.getKey().toString(), parameters); + } + } + + mediaInfo.close(); } - public void insert(Map>> mediaInfo) { - // create tabs for all streams - for (Entry>> entry : mediaInfo.entrySet()) { - for (Map parameters : entry.getValue()) { - JTable table = new JTable(new ParameterTableModel(parameters)); - - // allow sorting - table.setAutoCreateRowSorter(true); - - // sort by parameter name - table.getRowSorter().toggleSortOrder(0); - - addTab(entry.getKey().toString(), new JScrollPane(table)); - } - } + public void addTableTab(String title, Map data) { + JTable table = new JTable(new ParameterTableModel(data)); + + // allow sorting + table.setAutoCreateRowSorter(true); + + // sort by parameter name + table.getRowSorter().toggleSortOrder(0); + + addTab(title, new JScrollPane(table)); } public static void showMessageDialog(Component parent, File file) { final JDialog dialog = new JDialog(TunedUtilities.getWindow(parent), "MediaInfo", ModalityType.DOCUMENT_MODAL); - dialog.setLocation(TunedUtilities.getPreferredLocation(dialog)); + dialog.setLocationByPlatform(true); - JComponent c = (JComponent) dialog.getContentPane(); - c.setLayout(new MigLayout("fill", "[align center]", "[fill][pref!]")); - - MediaInfo mediaInfo = new MediaInfo(); - mediaInfo.open(file); - - MediaInfoComponent mediaInfoComponent = new MediaInfoComponent(mediaInfo.snapshot()); - - mediaInfo.close(); - - c.add(mediaInfoComponent, "grow, wrap"); - - c.add(new JButton(new AbstractAction("OK") { + Action closeAction = new AbstractAction("OK") { @Override public void actionPerformed(ActionEvent e) { dialog.setVisible(false); } - }), "wmin 80px, hmin 25px"); + }; + + JComponent c = (JComponent) dialog.getContentPane(); + c.setLayout(new MigLayout("fill", "[align center]", "[fill][pref!]")); + c.add(new MediaInfoPane(file), "grow, wrap"); + c.add(new JButton(closeAction), "wmin 80px, hmin 25px"); dialog.pack(); dialog.setVisible(true); } - protected static class ParameterTableModel extends AbstractTableModel { + private static class ParameterTableModel extends AbstractTableModel { private final List> data; diff --git a/source/net/sourceforge/filebot/ui/SelectButtonTextField.java b/source/net/sourceforge/filebot/ui/SelectButtonTextField.java index 09797818..02c9082a 100644 --- a/source/net/sourceforge/filebot/ui/SelectButtonTextField.java +++ b/source/net/sourceforge/filebot/ui/SelectButtonTextField.java @@ -53,8 +53,8 @@ public class SelectButtonTextField extends JComponent { editor.setRenderer(new CompletionCellRenderer()); editor.setUI(new TextFieldComboBoxUI()); - TunedUtilities.putActionForKeystroke(this, KeyStroke.getKeyStroke("ctrl UP"), new SpinClientAction(-1)); - TunedUtilities.putActionForKeystroke(this, KeyStroke.getKeyStroke("ctrl DOWN"), new SpinClientAction(1)); + TunedUtilities.installAction(this, KeyStroke.getKeyStroke("ctrl UP"), new SpinClientAction(-1)); + TunedUtilities.installAction(this, KeyStroke.getKeyStroke("ctrl DOWN"), new SpinClientAction(1)); } @@ -87,6 +87,7 @@ public class SelectButtonTextField extends JComponent { public SpinClientAction(int spin) { + super(String.format("Spin%+d", spin)); this.spin = spin; } diff --git a/source/net/sourceforge/filebot/ui/SelectDialog.java b/source/net/sourceforge/filebot/ui/SelectDialog.java index 1ae6a0a1..4b04a1d7 100644 --- a/source/net/sourceforge/filebot/ui/SelectDialog.java +++ b/source/net/sourceforge/filebot/ui/SelectDialog.java @@ -66,7 +66,7 @@ public class SelectDialog extends JDialog { setLocation(TunedUtilities.getPreferredLocation(this)); // Shortcut Enter - TunedUtilities.putActionForKeystroke(list, KeyStroke.getKeyStroke("released ENTER"), selectAction); + TunedUtilities.installAction(list, KeyStroke.getKeyStroke("released ENTER"), selectAction); } diff --git a/source/net/sourceforge/filebot/ui/panel/analyze/FileTreePanel.java b/source/net/sourceforge/filebot/ui/panel/analyze/FileTreePanel.java index dcbfd4bd..818a5bec 100644 --- a/source/net/sourceforge/filebot/ui/panel/analyze/FileTreePanel.java +++ b/source/net/sourceforge/filebot/ui/panel/analyze/FileTreePanel.java @@ -53,7 +53,7 @@ class FileTreePanel extends JComponent { }); // Shortcut DELETE - TunedUtilities.putActionForKeystroke(fileTree, KeyStroke.getKeyStroke("pressed DELETE"), removeAction); + TunedUtilities.installAction(fileTree, KeyStroke.getKeyStroke("pressed DELETE"), removeAction); } diff --git a/source/net/sourceforge/filebot/ui/panel/episodelist/EpisodeListPanel.java b/source/net/sourceforge/filebot/ui/panel/episodelist/EpisodeListPanel.java index 0958e2af..5739a2e2 100644 --- a/source/net/sourceforge/filebot/ui/panel/episodelist/EpisodeListPanel.java +++ b/source/net/sourceforge/filebot/ui/panel/episodelist/EpisodeListPanel.java @@ -64,8 +64,8 @@ public class EpisodeListPanel extends AbstractSearchPanel sequences() { - return Collections.unmodifiableList(sequences); + return sequences; } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/ValidateNamesDialog.java b/source/net/sourceforge/filebot/ui/panel/rename/ValidateNamesDialog.java index d910afeb..7944f26b 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/ValidateNamesDialog.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/ValidateNamesDialog.java @@ -72,7 +72,7 @@ class ValidateNamesDialog extends JDialog { setSize(365, 280); setLocation(TunedUtilities.getPreferredLocation(this)); - TunedUtilities.putActionForKeystroke(c, KeyStroke.getKeyStroke("released ESCAPE"), cancelAction); + TunedUtilities.installAction(c, KeyStroke.getKeyStroke("released ESCAPE"), cancelAction); } diff --git a/source/net/sourceforge/filebot/ui/panel/sfv/SfvPanel.java b/source/net/sourceforge/filebot/ui/panel/sfv/SfvPanel.java index c86632d3..042850fc 100644 --- a/source/net/sourceforge/filebot/ui/panel/sfv/SfvPanel.java +++ b/source/net/sourceforge/filebot/ui/panel/sfv/SfvPanel.java @@ -91,7 +91,7 @@ public class SfvPanel extends JComponent { putClientProperty("transferablePolicy", transferablePolicy); // Shortcut DELETE - TunedUtilities.putActionForKeystroke(this, KeyStroke.getKeyStroke("pressed DELETE"), removeAction); + TunedUtilities.installAction(this, KeyStroke.getKeyStroke("pressed DELETE"), removeAction); } diff --git a/source/net/sourceforge/tuned/ui/LazyDocumentListener.java b/source/net/sourceforge/tuned/ui/LazyDocumentListener.java new file mode 100644 index 00000000..89c4ab9e --- /dev/null +++ b/source/net/sourceforge/tuned/ui/LazyDocumentListener.java @@ -0,0 +1,60 @@ + +package net.sourceforge.tuned.ui; + + +import java.awt.event.ActionEvent; +import java.awt.event.ActionListener; + +import javax.swing.Timer; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; + + +public abstract class LazyDocumentListener implements DocumentListener { + + private DocumentEvent lastEvent; + + private final Timer timer = new Timer(200, new ActionListener() { + + @Override + public void actionPerformed(ActionEvent e) { + update(lastEvent); + + // we don't need it anymore + lastEvent = null; + } + }); + + + public LazyDocumentListener() { + timer.setRepeats(false); + } + + + private void defer(DocumentEvent e) { + lastEvent = e; + timer.restart(); + } + + + @Override + public void changedUpdate(DocumentEvent e) { + defer(e); + } + + + @Override + public void insertUpdate(DocumentEvent e) { + defer(e); + } + + + @Override + public void removeUpdate(DocumentEvent e) { + defer(e); + } + + + public abstract void update(DocumentEvent e); + +} diff --git a/source/net/sourceforge/tuned/ui/TunedUtilities.java b/source/net/sourceforge/tuned/ui/TunedUtilities.java index 5154e194..d69d7660 100644 --- a/source/net/sourceforge/tuned/ui/TunedUtilities.java +++ b/source/net/sourceforge/tuned/ui/TunedUtilities.java @@ -18,6 +18,7 @@ import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.lang.reflect.Method; +import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.Icon; import javax.swing.ImageIcon; @@ -29,6 +30,8 @@ import javax.swing.SwingUtilities; import javax.swing.Timer; import javax.swing.event.MouseInputListener; import javax.swing.plaf.basic.BasicTableUI; +import javax.swing.text.JTextComponent; +import javax.swing.undo.UndoManager; import net.sourceforge.tuned.ExceptionUtilities; @@ -50,13 +53,47 @@ public final class TunedUtilities { } - public static void putActionForKeystroke(JComponent component, KeyStroke keystroke, Action action) { - Integer key = action.hashCode(); + public static void installAction(JComponent component, KeyStroke keystroke, Action action) { + Object key = action.getValue(Action.NAME); + + if (key == null) + throw new IllegalArgumentException("Action must have a name"); + component.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(keystroke, key); component.getActionMap().put(key, action); } + public static UndoManager installUndoSupport(JTextComponent component) { + final UndoManager undoSupport = new UndoManager(); + + // install undo listener + component.getDocument().addUndoableEditListener(undoSupport); + + // install undo action + installAction(component, KeyStroke.getKeyStroke("control Z"), new AbstractAction("Undo") { + + @Override + public void actionPerformed(ActionEvent e) { + if (undoSupport.canUndo()) + undoSupport.undo(); + } + }); + + // install redo action + installAction(component, KeyStroke.getKeyStroke("control Y"), new AbstractAction("Redo") { + + @Override + public void actionPerformed(ActionEvent e) { + if (undoSupport.canRedo()) + undoSupport.redo(); + } + }); + + return undoSupport; + } + + public static Window getWindow(Component component) { if (component == null) return null;