1
0
mirror of https://github.com/mitb-archive/filebot synced 2024-12-24 16:58:51 -05:00

Rewrite ListPanel for parallel editing and testing of format expressions

This commit is contained in:
Reinhard Pointner 2016-03-20 18:33:31 +00:00
parent 56e13f072f
commit ef71e2fff8
17 changed files with 474 additions and 253 deletions

View File

@ -202,7 +202,7 @@ public class CmdlineOperations implements CmdlineInterface {
} }
// fetch episode data // fetch episode data
Collection<Episode> episodes = fetchEpisodeSet(db, seriesNames, sortOrder, locale, strict); List<Episode> episodes = fetchEpisodeSet(db, seriesNames, sortOrder, locale, strict);
if (episodes.size() == 0) { if (episodes.size() == 0) {
log.warning("Failed to fetch episode data: " + seriesNames); log.warning("Failed to fetch episode data: " + seriesNames);
continue; continue;
@ -277,7 +277,7 @@ public class CmdlineOperations implements CmdlineInterface {
return validMatches; return validMatches;
} }
private Set<Episode> fetchEpisodeSet(final EpisodeListProvider db, final Collection<String> names, final SortOrder sortOrder, final Locale locale, final boolean strict) throws Exception { private List<Episode> fetchEpisodeSet(final EpisodeListProvider db, final Collection<String> names, final SortOrder sortOrder, final Locale locale, final boolean strict) throws Exception {
Set<SearchResult> shows = new LinkedHashSet<SearchResult>(); Set<SearchResult> shows = new LinkedHashSet<SearchResult>();
Set<Episode> episodes = new LinkedHashSet<Episode>(); Set<Episode> episodes = new LinkedHashSet<Episode>();
@ -304,7 +304,7 @@ public class CmdlineOperations implements CmdlineInterface {
} }
} }
return episodes; return new ArrayList<Episode>(episodes);
} }
public List<File> renameMovie(Collection<File> files, RenameAction renameAction, ConflictAction conflictAction, File outputDir, ExpressionFormat format, MovieIdentificationService service, String query, ExpressionFilter filter, Locale locale, boolean strict) throws Exception { public List<File> renameMovie(Collection<File> files, RenameAction renameAction, ConflictAction conflictAction, File outputDir, ExpressionFormat format, MovieIdentificationService service, String query, ExpressionFilter filter, Locale locale, boolean strict) throws Exception {
@ -434,7 +434,7 @@ public class CmdlineOperations implements CmdlineInterface {
// unknown hash, try via imdb id from nfo file // unknown hash, try via imdb id from nfo file
if (movie == null) { if (movie == null) {
log.fine(format("Auto-detect movie from context: [%s]", file)); log.fine(format("Auto-detect movie from context: [%s]", file));
Collection<Movie> options = detectMovie(file, service, locale, strict); List<Movie> options = detectMovie(file, service, locale, strict);
// apply filter if defined // apply filter if defined
options = applyExpressionFilter(options, filter); options = applyExpressionFilter(options, filter);
@ -871,13 +871,13 @@ public class CmdlineOperations implements CmdlineInterface {
return destination; return destination;
} }
private <T> List<T> applyExpressionFilter(Collection<T> input, ExpressionFilter filter) throws Exception { private <T> List<T> applyExpressionFilter(List<T> input, ExpressionFilter filter) throws Exception {
if (filter == null) { if (filter == null) {
return new ArrayList<T>(input); return new ArrayList<T>(input);
} }
log.fine(format("Apply Filter: {%s}", filter.getExpression())); log.fine(format("Apply Filter: {%s}", filter.getExpression()));
Map<File, Object> context = new EntryList<File, Object>(null, input); Map<File, T> context = new EntryList<File, T>(null, input);
List<T> output = new ArrayList<T>(input.size()); List<T> output = new ArrayList<T>(input.size());
for (T it : input) { for (T it : input) {
if (filter.matches(new MediaBindingBean(it, null, context))) { if (filter.matches(new MediaBindingBean(it, null, context))) {

View File

@ -67,7 +67,7 @@ public class MediaBindingBean {
private final Object infoObject; private final Object infoObject;
private final File mediaFile; private final File mediaFile;
private final Map<File, Object> context; private final Map<File, ?> context;
private MediaInfo mediaInfo; private MediaInfo mediaInfo;
private Object metaInfo; private Object metaInfo;
@ -76,7 +76,7 @@ public class MediaBindingBean {
this(infoObject, mediaFile, singletonMap(mediaFile, infoObject)); this(infoObject, mediaFile, singletonMap(mediaFile, infoObject));
} }
public MediaBindingBean(Object infoObject, File mediaFile, Map<File, Object> context) { public MediaBindingBean(Object infoObject, File mediaFile, Map<File, ?> context) {
this.infoObject = infoObject; this.infoObject = infoObject;
this.mediaFile = mediaFile; this.mediaFile = mediaFile;
this.context = context; this.context = context;
@ -903,14 +903,14 @@ public class MediaBindingBean {
@Define("model") @Define("model")
public List<AssociativeScriptObject> getModel() { public List<AssociativeScriptObject> getModel() {
List<AssociativeScriptObject> result = new ArrayList<AssociativeScriptObject>(); List<AssociativeScriptObject> result = new ArrayList<AssociativeScriptObject>();
for (Entry<File, Object> it : context.entrySet()) { for (Entry<File, ?> it : context.entrySet()) {
result.add(createBindingObject(it.getKey(), it.getValue(), context)); result.add(createBindingObject(it.getKey(), it.getValue(), context));
} }
return result; return result;
} }
@Define("json") @Define("json")
public String getInfoObjectDump() throws Exception { public String getInfoObjectDump() {
return JsonWriter.objectToJson(infoObject); return JsonWriter.objectToJson(infoObject);
} }
@ -924,7 +924,7 @@ public class MediaBindingBean {
} else if (SUBTITLE_FILES.accept(getMediaFile()) || ((infoObject instanceof Episode || infoObject instanceof Movie) && !VIDEO_FILES.accept(getMediaFile()))) { } else if (SUBTITLE_FILES.accept(getMediaFile()) || ((infoObject instanceof Episode || infoObject instanceof Movie) && !VIDEO_FILES.accept(getMediaFile()))) {
// prefer equal match from current context if possible // prefer equal match from current context if possible
if (context != null) { if (context != null) {
for (Entry<File, Object> it : context.entrySet()) { for (Entry<File, ?> it : context.entrySet()) {
if (infoObject.equals(it.getValue()) && VIDEO_FILES.accept(it.getKey())) { if (infoObject.equals(it.getValue()) && VIDEO_FILES.accept(it.getKey())) {
return it.getKey(); return it.getKey();
} }
@ -994,7 +994,7 @@ public class MediaBindingBean {
return undefined(String.format("%s[%d][%s]", streamKind, streamNumber, join(keys, ", "))); return undefined(String.format("%s[%d][%s]", streamKind, streamNumber, join(keys, ", ")));
} }
private AssociativeScriptObject createBindingObject(File file, Object info, Map<File, Object> context) { private AssociativeScriptObject createBindingObject(File file, Object info, Map<File, ?> context) {
MediaBindingBean mediaBindingBean = new MediaBindingBean(info, file, context) { MediaBindingBean mediaBindingBean = new MediaBindingBean(info, file, context) {
@Override @Override
@Define(undefined) @Define(undefined)

View File

@ -1,6 +1,7 @@
package net.filebot.torrent; package net.filebot.torrent;
import static java.nio.charset.StandardCharsets.*; import static java.nio.charset.StandardCharsets.*;
import static java.util.Collections.*;
import java.io.BufferedInputStream; import java.io.BufferedInputStream;
import java.io.File; import java.io.File;
@ -9,11 +10,13 @@ import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import net.filebot.vfs.FileInfo;
import net.filebot.vfs.SimpleFileInfo;
public class Torrent { public class Torrent {
private String name; private String name;
@ -24,7 +27,7 @@ public class Torrent {
private Long creationDate; private Long creationDate;
private Long pieceLength; private Long pieceLength;
private List<Entry> files; private List<FileInfo> files;
private boolean singleFileTorrent; private boolean singleFileTorrent;
protected Torrent() { protected Torrent() {
@ -59,7 +62,7 @@ public class Torrent {
// torrent contains multiple entries // torrent contains multiple entries
singleFileTorrent = false; singleFileTorrent = false;
List<Entry> entries = new ArrayList<Entry>(); List<FileInfo> entries = new ArrayList<FileInfo>();
for (Object fileMapObject : (List<?>) infoMap.get("files")) { for (Object fileMapObject : (List<?>) infoMap.get("files")) {
Map<?, ?> fileMap = (Map<?, ?>) fileMapObject; Map<?, ?> fileMap = (Map<?, ?>) fileMapObject;
@ -81,17 +84,17 @@ public class Torrent {
Long length = decodeLong(fileMap.get("length")); Long length = decodeLong(fileMap.get("length"));
entries.add(new Entry(path.toString(), length)); entries.add(new SimpleFileInfo(path.toString(), length));
} }
files = Collections.unmodifiableList(entries); files = unmodifiableList(entries);
} else { } else {
// single file torrent // single file torrent
singleFileTorrent = true; singleFileTorrent = true;
Long length = decodeLong(infoMap.get("length")); Long length = decodeLong(infoMap.get("length"));
files = Collections.singletonList(new Entry(name, length)); files = singletonList(new SimpleFileInfo(name, length));
} }
} }
@ -139,7 +142,7 @@ public class Torrent {
return encoding; return encoding;
} }
public List<Entry> getFiles() { public List<FileInfo> getFiles() {
return files; return files;
} }
@ -155,35 +158,4 @@ public class Torrent {
return singleFileTorrent; return singleFileTorrent;
} }
public static class Entry {
private final String path;
private final long length;
public Entry(String path, long length) {
this.path = path;
this.length = length;
}
public String getPath() {
return path;
}
public String getName() {
// the last element in the path is the filename
// torrents don't contain directory entries, so there is always a non-empty name
return path.substring(path.lastIndexOf("/") + 1);
}
public long getLength() {
return length;
}
@Override
public String toString() {
return getPath();
}
}
} }

View File

@ -1,35 +1,39 @@
package net.filebot.ui; package net.filebot.ui;
import java.awt.Cursor;
import java.io.PrintWriter; import java.io.PrintWriter;
import net.filebot.ui.transfer.TextFileExportHandler; import net.filebot.ui.transfer.TextFileExportHandler;
public class FileBotListExportHandler<T> extends TextFileExportHandler {
public class FileBotListExportHandler extends TextFileExportHandler { protected final FileBotList<T> list;
protected final FileBotList<?> list; public FileBotListExportHandler(FileBotList<T> list) {
public FileBotListExportHandler(FileBotList<?> list) {
this.list = list; this.list = list;
} }
@Override @Override
public boolean canExport() { public boolean canExport() {
return list.getModel().size() > 0; return list.getModel().size() > 0;
} }
@Override @Override
public void export(PrintWriter out) { public void export(PrintWriter out) {
for (Object entry : list.getModel()) { try {
out.println(entry); list.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
for (T item : list.getModel()) {
export(item, out);
}
} finally {
list.setCursor(Cursor.getDefaultCursor());
} }
} }
public void export(T item, PrintWriter out) {
out.println(item);
}
@Override @Override
public String getDefaultFileName() { public String getDefaultFileName() {

View File

@ -278,7 +278,7 @@ public class EpisodeListPanel extends AbstractSearchPanel<EpisodeListProvider, E
} }
protected static class EpisodeListExportHandler extends FileBotListExportHandler implements ClipboardHandler { protected static class EpisodeListExportHandler extends FileBotListExportHandler<Episode> implements ClipboardHandler {
public EpisodeListExportHandler(FileBotList<Episode> list) { public EpisodeListExportHandler(FileBotList<Episode> list) {
super(list); super(list);

View File

@ -1,25 +1,60 @@
package net.filebot.ui.list; package net.filebot.ui.list;
import static java.util.Arrays.*;
import static java.util.Collections.*;
import static java.util.stream.Collectors.*;
import static net.filebot.MediaTypes.*; import static net.filebot.MediaTypes.*;
import static net.filebot.ui.transfer.FileTransferable.*;
import static net.filebot.util.FileUtilities.*; import static net.filebot.util.FileUtilities.*;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.function.Consumer;
import net.filebot.torrent.Torrent; import net.filebot.torrent.Torrent;
import net.filebot.ui.FileBotList; import net.filebot.ui.transfer.ArrayTransferable;
import net.filebot.ui.transfer.FileTransferablePolicy; import net.filebot.ui.transfer.FileTransferablePolicy;
import net.filebot.util.FileUtilities;
import net.filebot.util.FileUtilities.ExtensionFileFilter; import net.filebot.util.FileUtilities.ExtensionFileFilter;
import net.filebot.web.Episode;
class FileListTransferablePolicy extends FileTransferablePolicy { class FileListTransferablePolicy extends FileTransferablePolicy {
private FileBotList<? super String> list; private static final DataFlavor episodeArrayFlavor = ArrayTransferable.flavor(Episode.class);
public FileListTransferablePolicy(FileBotList<? super String> list) { private Consumer<String> title;
this.list = list; private Consumer<String> format;
private Consumer<List<?>> model;
public FileListTransferablePolicy(Consumer<String> title, Consumer<String> format, Consumer<List<?>> model) {
this.title = title;
this.format = format;
this.model = model;
}
@Override
public boolean accept(Transferable tr) throws Exception {
return hasFileListFlavor(tr) || tr.isDataFlavorSupported(episodeArrayFlavor);
}
@Override
public void handleTransferable(Transferable tr, TransferAction action) throws Exception {
// handle episode data
if (tr.isDataFlavorSupported(episodeArrayFlavor)) {
Episode[] episodes = (Episode[]) tr.getTransferData((episodeArrayFlavor));
if (episodes.length > 0) {
format.accept(ListPanel.DEFAULT_EPISODE_FORMAT);
title.accept(episodes[0].getSeriesName());
model.accept(asList(episodes));
}
return;
}
// handle files
super.handleTransferable(tr, action);
} }
@Override @Override
@ -29,48 +64,44 @@ class FileListTransferablePolicy extends FileTransferablePolicy {
@Override @Override
protected void clear() { protected void clear() {
list.getModel().clear(); format.accept("");
title.accept("");
model.accept(emptyList());
} }
@Override @Override
protected void load(List<File> files, TransferAction action) throws IOException { protected void load(List<File> files, TransferAction action) throws IOException {
// set title based on parent folder of first file // set title based on parent folder of first file
list.setTitle(FileUtilities.getFolderName(files.get(0).getParentFile())); title.accept(getFolderName(files.get(0).getParentFile()));
// clear selection
list.getListComponent().clearSelection();
if (containsOnly(files, TORRENT_FILES)) { if (containsOnly(files, TORRENT_FILES)) {
loadTorrents(files); loadTorrents(files);
} else { } else {
// if only one folder was dropped, use its name as title // if only one folder was dropped, use its name as title
if (files.size() == 1 && files.get(0).isDirectory()) { if (files.size() == 1 && files.get(0).isDirectory()) {
list.setTitle(FileUtilities.getFolderName(files.get(0))); title.accept(getFolderName(files.get(0)));
} }
// load all files from the given folders recursively up do a depth of 32 // load all files from the given folders recursively up do a depth of 32
for (File file : listFiles(files)) { format.accept(ListPanel.DEFAULT_FILE_FORMAT);
list.getModel().add(FileUtilities.getName(file)); model.accept(listFiles(files));
}
} }
} }
private void loadTorrents(List<File> files) throws IOException { private void loadTorrents(List<File> files) throws IOException {
List<Torrent> torrents = new ArrayList<Torrent>(files.size()); List<Torrent> torrents = new ArrayList<Torrent>(files.size());
for (File file : files) { for (File file : files) {
torrents.add(new Torrent(file)); torrents.add(new Torrent(file));
} }
if (torrents.size() == 1) { // set title
list.setTitle(FileUtilities.getNameWithoutExtension(torrents.get(0).getName())); if (torrents.size() > 0) {
title.accept(getNameWithoutExtension(torrents.get(0).getName()));
} }
for (Torrent torrent : torrents) { // add torrent entries
for (Torrent.Entry entry : torrent.getFiles()) { format.accept(ListPanel.DEFAULT_FILE_FORMAT);
list.getModel().add(FileUtilities.getNameWithoutExtension(entry.getName())); model.accept(torrents.stream().flatMap(t -> t.getFiles().stream()).collect(toList()));
}
}
} }
@Override @Override

View File

@ -0,0 +1,50 @@
package net.filebot.ui.list;
import static net.filebot.util.FileUtilities.*;
import java.io.File;
import java.util.List;
import java.util.Map;
import net.filebot.format.Define;
import net.filebot.format.MediaBindingBean;
import net.filebot.util.EntryList;
import net.filebot.util.FunctionList;
public class IndexedBindingBean extends MediaBindingBean {
private int i;
private int from;
private int to;
public IndexedBindingBean(Object object, int i, int from, int to, List<?> context) {
super(object, getMediaFile(object), getContext(context));
this.i = i;
this.from = from;
this.to = to;
}
@Define("i")
public Integer getModelIndex() {
return i;
}
@Define("from")
public Integer getFromIndex() {
return from;
}
@Define("to")
public Integer getToIndex() {
return to;
}
private static File getMediaFile(Object object) {
return object instanceof File ? (File) object : new File(object.toString());
}
private static Map<File, Object> getContext(List<?> context) {
return new EntryList<File, Object>(new FunctionList<Object, File>((List<Object>) context, IndexedBindingBean::getMediaFile), context);
}
}

View File

@ -0,0 +1,46 @@
package net.filebot.ui.list;
import net.filebot.format.ExpressionFormat;
public class ListItem {
private IndexedBindingBean bindings;
private ExpressionFormat format;
private String value;
public ListItem(IndexedBindingBean bindings, ExpressionFormat format) {
this.bindings = bindings;
this.format = format;
this.value = format != null ? null : bindings.getInfoObject().toString();
}
public IndexedBindingBean getBindings() {
return bindings;
}
public Object getObject() {
return bindings.getInfoObject();
}
public ExpressionFormat getFormat() {
return format;
}
public String getFormattedValue() {
if (value == null) {
value = format.format(bindings);
if (value == null && format.caughtScriptException() != null) {
value = format.caughtScriptException().getMessage();
}
}
return value;
}
@Override
public String toString() {
return getObject().toString();
}
}

View File

@ -1,34 +1,38 @@
package net.filebot.ui.list; package net.filebot.ui.list;
import static java.awt.Font.*; import static java.awt.Font.*;
import static java.lang.Math.*; import static java.util.stream.Collectors.*;
import static net.filebot.Logging.*; import static javax.swing.BorderFactory.*;
import static net.filebot.media.MediaDetection.*;
import static net.filebot.util.ui.SwingUI.*; import static net.filebot.util.ui.SwingUI.*;
import java.awt.BorderLayout; import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Font; import java.awt.Font;
import java.awt.datatransfer.Transferable; import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent; import java.io.PrintWriter;
import java.awt.event.KeyEvent;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List; import java.util.List;
import java.util.logging.Level; import java.util.ListIterator;
import java.util.stream.IntStream;
import javax.script.Bindings; import javax.script.ScriptException;
import javax.script.SimpleBindings;
import javax.swing.JButton; import javax.swing.JButton;
import javax.swing.JComponent; import javax.swing.JComponent;
import javax.swing.JLabel; import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel; import javax.swing.JPanel;
import javax.swing.JSpinner; import javax.swing.JSpinner;
import javax.swing.JSpinner.NumberEditor; import javax.swing.JSpinner.NumberEditor;
import javax.swing.JTextField; import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.SpinnerNumberModel; import javax.swing.SpinnerNumberModel;
import javax.swing.SwingUtilities;
import javax.swing.event.DocumentEvent;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import org.fife.ui.rsyntaxtextarea.RSyntaxDocument;
import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea;
import org.fife.ui.rsyntaxtextarea.SyntaxConstants;
import org.fife.ui.rtextarea.RTextScrollPane;
import com.google.common.eventbus.Subscribe; import com.google.common.eventbus.Subscribe;
@ -40,42 +44,92 @@ import net.filebot.ui.transfer.LoadAction;
import net.filebot.ui.transfer.SaveAction; import net.filebot.ui.transfer.SaveAction;
import net.filebot.ui.transfer.TransferablePolicy; import net.filebot.ui.transfer.TransferablePolicy;
import net.filebot.ui.transfer.TransferablePolicy.TransferAction; import net.filebot.ui.transfer.TransferablePolicy.TransferAction;
import net.filebot.util.ExceptionUtilities; import net.filebot.util.ui.DefaultFancyListCellRenderer;
import net.filebot.util.ui.LazyDocumentListener;
import net.filebot.util.ui.PrototypeCellValueUpdater;
import net.miginfocom.swing.MigLayout; import net.miginfocom.swing.MigLayout;
public class ListPanel extends JComponent { public class ListPanel extends JComponent {
private FileBotList<String> list = new FileBotList<String>(); public static final String DEFAULT_SEQUENCE_FORMAT = "Sequence - {i.pad(2)}";
public static final String DEFAULT_FILE_FORMAT = "{fn}";
public static final String DEFAULT_EPISODE_FORMAT = "{n} - {s00e00} - [{airdate.format(/dd MMM YYYY/)}] - {t}";
private JTextField textField = new JTextField("Name - {i}", 30); private RSyntaxTextArea editor = createEditor();
private SpinnerNumberModel fromSpinnerModel = new SpinnerNumberModel(1, 0, Integer.MAX_VALUE, 1); private SpinnerNumberModel fromSpinnerModel = new SpinnerNumberModel(1, 0, Integer.MAX_VALUE, 1);
private SpinnerNumberModel toSpinnerModel = new SpinnerNumberModel(20, 0, Integer.MAX_VALUE, 1); private SpinnerNumberModel toSpinnerModel = new SpinnerNumberModel(20, 0, Integer.MAX_VALUE, 1);
private FileBotList<ListItem> list = new FileBotList<ListItem>();
public ListPanel() { public ListPanel() {
list.setTitle("Title"); list.setTitle("Title");
textField.setFont(new Font(MONOSPACED, PLAIN, 11)); // need a fixed cell size for high performance scrolling
list.getListComponent().setFixedCellHeight(28);
list.setTransferablePolicy(new FileListTransferablePolicy(list)); list.getListComponent().getModel().addListDataListener(new PrototypeCellValueUpdater(list.getListComponent(), ""));
list.setExportHandler(new FileBotListExportHandler(list));
list.getRemoveAction().setEnabled(true); list.getRemoveAction().setEnabled(true);
list.setTransferablePolicy(new FileListTransferablePolicy(list::setTitle, editor::setText, this::createItemSequence));
list.setExportHandler(new FileBotListExportHandler<ListItem>(list) {
@Override
public void export(ListItem item, PrintWriter out) {
out.println(item.getFormattedValue());
}
});
list.getListComponent().setCellRenderer(new DefaultFancyListCellRenderer() {
@Override
protected void configureListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
super.configureListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
ListItem item = (ListItem) value;
String text = item.getFormattedValue(); // format just-in-time
if (text.isEmpty()) {
if (item.getFormat() != null && item.getFormat().caughtScriptException() != null) {
setText(item.getFormat().caughtScriptException().getMessage());
} else {
setText("Expression yields no results for value " + item.getObject());
}
setIcon(ResourceManager.getIcon("status.warning"));
} else {
setText(text);
setIcon(null);
}
}
});
JSpinner fromSpinner = new JSpinner(fromSpinnerModel); JSpinner fromSpinner = new JSpinner(fromSpinnerModel);
JSpinner toSpinner = new JSpinner(toSpinnerModel); JSpinner toSpinner = new JSpinner(toSpinnerModel);
fromSpinner.setEditor(new NumberEditor(fromSpinner, "#")); fromSpinner.setEditor(new NumberEditor(fromSpinner, "#"));
toSpinner.setEditor(new NumberEditor(toSpinner, "#")); toSpinner.setEditor(new NumberEditor(toSpinner, "#"));
RTextScrollPane editorScrollPane = new RTextScrollPane(editor, false);
editorScrollPane.setLineNumbersEnabled(false);
editorScrollPane.setFoldIndicatorEnabled(false);
editorScrollPane.setIconRowHeaderEnabled(false);
editorScrollPane.setVerticalScrollBarPolicy(RTextScrollPane.VERTICAL_SCROLLBAR_NEVER);
editorScrollPane.setHorizontalScrollBarPolicy(RTextScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
editorScrollPane.setBackground(editor.getBackground());
editorScrollPane.setViewportBorder(createEmptyBorder(2, 2, 2, 2));
editorScrollPane.setOpaque(true);
editorScrollPane.setBorder(new JTextField().getBorder());
setLayout(new MigLayout("nogrid, fill, insets dialog", "align center", "[pref!, center][fill]")); setLayout(new MigLayout("nogrid, fill, insets dialog", "align center", "[pref!, center][fill]"));
add(new JLabel("Pattern:"), "gapbefore indent"); JLabel patternLabel = new JLabel("Pattern:");
add(textField, "gap related, wmin 2cm, sizegroupy editor"); add(patternLabel, "gapbefore indent");
add(editorScrollPane, "gap related, growx, wmin 2cm, h pref!, sizegroupy editor");
add(new JLabel("From:"), "gap 5mm"); add(new JLabel("From:"), "gap 5mm");
add(fromSpinner, "gap related, wmax 14mm, sizegroup spinner, sizegroupy editor"); add(fromSpinner, "gap related, wmax 15mm, sizegroup spinner, sizegroupy editor");
add(new JLabel("To:"), "gap 5mm"); add(new JLabel("To:"), "gap 5mm");
add(toSpinner, "gap related, wmax 14mm, sizegroup spinner, sizegroupy editor"); add(toSpinner, "gap related, wmax 15mm, sizegroup spinner, sizegroupy editor");
add(newButton("Create", ResourceManager.getIcon("action.export"), this::create), "gap 7mm, gapafter indent, wrap paragraph"); add(newButton("Sequence", ResourceManager.getIcon("action.export"), evt -> createItemSequence()), "gap 7mm, gapafter indent, wrap paragraph");
add(list, "grow"); add(list, "grow");
@ -86,57 +140,92 @@ public class ListPanel extends JComponent {
list.add(buttonPanel, BorderLayout.SOUTH); list.add(buttonPanel, BorderLayout.SOUTH);
installAction(this, KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), newAction("Create", this::create)); // initialize with default values
SwingUtilities.invokeLater(() -> {
if (list.getModel().isEmpty()) {
createItemSequence();
}
});
} }
public void create(ActionEvent evt) { private RSyntaxTextArea createEditor() {
// clear selection RSyntaxTextArea editor = new RSyntaxTextArea(new RSyntaxDocument(SyntaxConstants.SYNTAX_STYLE_GROOVY) {
list.getListComponent().clearSelection(); @Override
public void insertString(int offs, String str, AttributeSet a) throws BadLocationException {
super.insertString(offs, str.replaceAll("\\R", ""), a); // FORCE SINGLE LINE
}
}, null, 1, 80);
editor.setAntiAliasingEnabled(true);
editor.setAnimateBracketMatching(false);
editor.setAutoIndentEnabled(false);
editor.setClearWhitespaceLinesEnabled(false);
editor.setBracketMatchingEnabled(true);
editor.setCloseCurlyBraces(false);
editor.setCodeFoldingEnabled(false);
editor.setHyperlinksEnabled(false);
editor.setUseFocusableTips(false);
editor.setHighlightCurrentLine(false);
editor.setLineWrap(false);
editor.setFont(new Font(MONOSPACED, PLAIN, 14));
// update format on change
editor.getDocument().addDocumentListener(new LazyDocumentListener(20) {
private Color valid = editor.getForeground();
private Color invalid = Color.red;
@Override
public void update(DocumentEvent evt) {
try {
String expression = editor.getText().trim();
setFormat(expression.isEmpty() ? null : new ExpressionFormat(expression));
editor.setForeground(valid);
} catch (ScriptException e) {
editor.setForeground(invalid);
}
}
});
return editor;
}
private ExpressionFormat format;
public ListItem createItem(Object object, int i, int from, int to, List<?> context) {
return new ListItem(new IndexedBindingBean(object, i, from, to, context), format);
}
public void setFormat(ExpressionFormat format) {
this.format = format;
// update items
for (ListIterator<ListItem> itr = list.getModel().listIterator(); itr.hasNext();) {
itr.set(new ListItem(itr.next().getBindings(), format));
}
}
public void createItemSequence(List<?> objects) {
List<ListItem> items = IntStream.range(0, objects.size()).mapToObj(i -> createItem(objects.get(i), i, 0, objects.size(), objects)).collect(toList());
list.getListComponent().clearSelection();
list.getModel().clear();
list.getModel().addAll(items);
}
public void createItemSequence() {
int from = fromSpinnerModel.getNumber().intValue(); int from = fromSpinnerModel.getNumber().intValue();
int to = toSpinnerModel.getNumber().intValue(); int to = toSpinnerModel.getNumber().intValue();
try { List<Integer> context = IntStream.rangeClosed(from, to).boxed().collect(toList());
ExpressionFormat format = new ExpressionFormat(textField.getText()); List<ListItem> items = context.stream().map(it -> createItem(it, it.intValue(), from, to, context)).collect(toList());
// pad episode numbers with zeros (e.g. %02d) so all numbers have the same number of digits
NumberFormat numberFormat = NumberFormat.getIntegerInstance();
numberFormat.setMinimumIntegerDigits(max(2, Integer.toString(max(from, to)).length()));
numberFormat.setGroupingUsed(false);
List<String> names = new ArrayList<String>();
int min = min(from, to);
int max = max(from, to);
for (int i = min; i <= max; i++) {
Bindings bindings = new SimpleBindings();
// strings
bindings.put("i", numberFormat.format(i));
// numbers
bindings.put("index", i);
bindings.put("from", from);
bindings.put("to", to);
names.add(format.format(bindings));
}
if (signum(to - from) < 0) {
Collections.reverse(names);
}
// try to match title from the first five names
Collection<String> title = getSeriesNameMatcher(true).matchAll((names.size() < 5 ? names : names.subList(0, 4)).toArray(new String[0]));
list.setTitle(title.isEmpty() ? "List" : title.iterator().next());
editor.setText(DEFAULT_SEQUENCE_FORMAT);
list.setTitle("Sequence");
list.getListComponent().clearSelection();
list.getModel().clear(); list.getModel().clear();
list.getModel().addAll(names); list.getModel().addAll(items);
} catch (Exception e) {
log.log(Level.WARNING, ExceptionUtilities.getMessage(e), e);
}
} }
@Subscribe @Subscribe

View File

@ -50,9 +50,9 @@ import javax.swing.JLabel;
import javax.swing.JMenuItem; import javax.swing.JMenuItem;
import javax.swing.JPanel; import javax.swing.JPanel;
import javax.swing.JPopupMenu; import javax.swing.JPopupMenu;
import javax.swing.JTextField;
import javax.swing.SwingWorker; import javax.swing.SwingWorker;
import javax.swing.Timer; import javax.swing.Timer;
import javax.swing.border.EmptyBorder;
import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentEvent;
import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuEvent;
import javax.swing.event.PopupMenuListener; import javax.swing.event.PopupMenuListener;
@ -206,11 +206,11 @@ public class FormatDialog extends JDialog {
editorScrollPane.setVerticalScrollBarPolicy(RTextScrollPane.VERTICAL_SCROLLBAR_NEVER); editorScrollPane.setVerticalScrollBarPolicy(RTextScrollPane.VERTICAL_SCROLLBAR_NEVER);
editorScrollPane.setHorizontalScrollBarPolicy(RTextScrollPane.HORIZONTAL_SCROLLBAR_NEVER); editorScrollPane.setHorizontalScrollBarPolicy(RTextScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
editorScrollPane.setViewportBorder(new EmptyBorder(7, 0, 7, 0)); editorScrollPane.setViewportBorder(createEmptyBorder(7, 2, 7, 2));
editorScrollPane.setBackground(editor.getBackground());
editorScrollPane.setOpaque(true); editorScrollPane.setOpaque(true);
editorScrollPane.setBorder(new JTextField().getBorder());
content.add(editorScrollPane, "w 120px:min(pref, 420px), h 40px!, growx, wrap 4px, id editor"); content.add(editorScrollPane, "w 120px:min(pref, 420px), h pref!, growx, wrap 4px, id editor");
content.add(createImageButton(changeSampleAction), "sg action, w 25!, h 19!, pos n editor.y2+2 editor.x2 n"); content.add(createImageButton(changeSampleAction), "sg action, w 25!, h 19!, pos n editor.y2+2 editor.x2 n");
content.add(createImageButton(selectFolderAction), "sg action, w 25!, h 19!, pos n editor.y2+2 editor.x2-(27*1) n"); content.add(createImageButton(selectFolderAction), "sg action, w 25!, h 19!, pos n editor.y2+2 editor.x2-(27*1) n");
content.add(createImageButton(showRecentAction), "sg action, w 25!, h 19!, pos n editor.y2+2 editor.x2-(27*2) n"); content.add(createImageButton(showRecentAction), "sg action, w 25!, h 19!, pos n editor.y2+2 editor.x2-(27*2) n");

View File

@ -145,10 +145,7 @@ class NamesListTransferablePolicy extends FileTransferablePolicy {
protected void loadTorrentFiles(List<File> files, List<Object> values) throws IOException { protected void loadTorrentFiles(List<File> files, List<Object> values) throws IOException {
for (File file : files) { for (File file : files) {
Torrent torrent = new Torrent(file); Torrent torrent = new Torrent(file);
values.addAll(torrent.getFiles());
for (Torrent.Entry entry : torrent.getFiles()) {
values.add(new SimpleFileInfo(entry.getPath(), entry.getLength()));
}
} }
} }

View File

@ -11,16 +11,14 @@ import java.awt.event.MouseEvent;
import javax.swing.Action; import javax.swing.Action;
import javax.swing.JButton; import javax.swing.JButton;
import javax.swing.JPanel; import javax.swing.JPanel;
import javax.swing.ListModel;
import javax.swing.ListSelectionModel; import javax.swing.ListSelectionModel;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import ca.odell.glazedlists.EventList; import ca.odell.glazedlists.EventList;
import net.filebot.ResourceManager; import net.filebot.ResourceManager;
import net.filebot.ui.FileBotList; import net.filebot.ui.FileBotList;
import net.filebot.ui.transfer.LoadAction; import net.filebot.ui.transfer.LoadAction;
import net.filebot.util.ui.ActionPopup; import net.filebot.util.ui.ActionPopup;
import net.filebot.util.ui.PrototypeCellValueUpdater;
import net.miginfocom.swing.MigLayout; import net.miginfocom.swing.MigLayout;
class RenameList<E> extends FileBotList<E> { class RenameList<E> extends FileBotList<E> {
@ -36,43 +34,7 @@ class RenameList<E> extends FileBotList<E> {
// need a fixed cell size for high performance scrolling // need a fixed cell size for high performance scrolling
list.setFixedCellHeight(28); list.setFixedCellHeight(28);
list.getModel().addListDataListener(new ListDataListener() { list.getModel().addListDataListener(new PrototypeCellValueUpdater(list, ""));
private int longestItemLength = -1;
@Override
public void intervalRemoved(ListDataEvent evt) {
// reset prototype value
ListModel<?> m = (ListModel<?>) evt.getSource();
if (m.getSize() == 0) {
longestItemLength = -1;
list.setPrototypeCellValue(null);
}
}
@Override
public void intervalAdded(ListDataEvent evt) {
contentsChanged(evt);
}
@Override
public void contentsChanged(ListDataEvent evt) {
ListModel<?> m = (ListModel<?>) evt.getSource();
for (int i = evt.getIndex0(); i <= evt.getIndex1() && i < m.getSize(); i++) {
Object item = m.getElementAt(i);
int itemLength = item.toString().length();
if (itemLength > longestItemLength) {
// cell values will not be updated if the prototype object remains the same (even if the object has changed) so we need to reset it
if (item == list.getPrototypeCellValue()) {
list.setPrototypeCellValue("");
}
longestItemLength = itemLength;
list.setPrototypeCellValue(item);
}
}
}
});
list.addMouseListener(dndReorderMouseAdapter); list.addMouseListener(dndReorderMouseAdapter);
list.addMouseMotionListener(dndReorderMouseAdapter); list.addMouseMotionListener(dndReorderMouseAdapter);

View File

@ -1,7 +1,6 @@
package net.filebot.ui.transfer; package net.filebot.ui.transfer;
import java.awt.datatransfer.Transferable; import java.awt.datatransfer.Transferable;
import java.io.File; import java.io.File;
import java.io.IOException; import java.io.IOException;
@ -11,38 +10,28 @@ import java.io.StringWriter;
import javax.swing.JComponent; import javax.swing.JComponent;
import javax.swing.TransferHandler; import javax.swing.TransferHandler;
public abstract class TextFileExportHandler implements TransferableExportHandler, FileExportHandler { public abstract class TextFileExportHandler implements TransferableExportHandler, FileExportHandler {
@Override @Override
public abstract boolean canExport(); public abstract boolean canExport();
public abstract void export(PrintWriter out); public abstract void export(PrintWriter out);
@Override @Override
public abstract String getDefaultFileName(); public abstract String getDefaultFileName();
@Override @Override
public void export(File file) throws IOException { public void export(File file) throws IOException {
PrintWriter out = new PrintWriter(file, "UTF-8"); try (PrintWriter out = new PrintWriter(file, "UTF-8")) {
try {
export(out); export(out);
} finally {
out.close();
} }
} }
@Override @Override
public int getSourceActions(JComponent c) { public int getSourceActions(JComponent c) {
return canExport() ? TransferHandler.COPY_OR_MOVE : TransferHandler.NONE; return canExport() ? TransferHandler.COPY_OR_MOVE : TransferHandler.NONE;
} }
@Override @Override
public Transferable createTransferable(JComponent c) { public Transferable createTransferable(JComponent c) {
// get transfer data // get transfer data
@ -52,7 +41,6 @@ public abstract class TextFileExportHandler implements TransferableExportHandler
return new TextFileTransferable(getDefaultFileName(), buffer.toString()); return new TextFileTransferable(getDefaultFileName(), buffer.toString());
} }
@Override @Override
public void exportDone(JComponent source, Transferable data, int action) { public void exportDone(JComponent source, Transferable data, int action) {

View File

@ -2,27 +2,20 @@ package net.filebot.util;
import static java.util.Collections.*; import static java.util.Collections.*;
import java.util.AbstractList;
import java.util.AbstractMap; import java.util.AbstractMap;
import java.util.AbstractSet; import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Iterator; import java.util.Iterator;
import java.util.List; import java.util.List;
import java.util.Set; import java.util.Set;
public class EntryList<K, V> extends AbstractMap<K, V> { public class EntryList<K, V> extends AbstractMap<K, V> {
private final List<Entry<K, V>> entryList = new ArrayList<Entry<K, V>>(); private List<? extends K> keys;
private List<? extends V> values;
public EntryList(Iterable<? extends K> keys, Iterable<? extends V> values) { public EntryList(List<? extends K> keys, List<? extends V> values) {
Iterator<? extends K> keySeq = keys != null ? keys.iterator() : emptyIterator(); this.keys = keys != null ? keys : emptyList();
Iterator<? extends V> valueSeq = values != null ? values.iterator() : emptyIterator(); this.values = values != null ? values : emptyList();
while (keySeq.hasNext() || valueSeq.hasNext()) {
K key = keySeq.hasNext() ? keySeq.next() : null;
V value = valueSeq.hasNext() ? valueSeq.next() : null;
entryList.add(new SimpleImmutableEntry<K, V>(key, value));
}
} }
@Override @Override
@ -31,35 +24,56 @@ public class EntryList<K, V> extends AbstractMap<K, V> {
@Override @Override
public Iterator<Entry<K, V>> iterator() { public Iterator<Entry<K, V>> iterator() {
return entryList.iterator(); return new Iterator<Entry<K, V>>() {
private Iterator<? extends K> keySeq = keys.iterator();
private Iterator<? extends V> valueSeq = values.iterator();
@Override
public boolean hasNext() {
return keySeq.hasNext() || valueSeq.hasNext();
}
@Override
public Entry<K, V> next() {
K key = keySeq.hasNext() ? keySeq.next() : null;
V value = valueSeq.hasNext() ? valueSeq.next() : null;
return new SimpleImmutableEntry<K, V>(key, value);
}
};
} }
@Override @Override
public int size() { public int size() {
return entryList.size(); return keys.size();
}
};
}
@Override
public Set<K> keySet() {
return new AbstractSet<K>() {
@Override
public Iterator<K> iterator() {
return (Iterator<K>) keys.iterator();
}
@Override
public int size() {
return keys.size();
} }
}; };
} }
@Override @Override
public List<V> values() { public List<V> values() {
return new AbstractList<V>() { return (List<V>) values;
@Override
public V get(int index) {
return entryList.get(index).getValue();
} }
@Override @Override
public int size() { public int size() {
return entryList.size(); return Math.max(keys.size(), values.size());
}
};
}
@Override
public int size() {
return entryList.size();
} }
} }

View File

@ -0,0 +1,27 @@
package net.filebot.util;
import java.util.AbstractList;
import java.util.List;
import java.util.function.Function;
public class FunctionList<S, E> extends AbstractList<E> {
private List<S> source;
private Function<S, E> function;
public FunctionList(List<S> source, Function<S, E> function) {
this.source = source;
this.function = function;
}
@Override
public E get(int index) {
return function.apply(source.get(index));
}
@Override
public int size() {
return source.size();
}
}

View File

@ -1,7 +1,6 @@
package net.filebot.util.ui; package net.filebot.util.ui;
import java.awt.Color; import java.awt.Color;
import java.awt.Insets; import java.awt.Insets;
@ -10,63 +9,52 @@ import javax.swing.Icon;
import javax.swing.JLabel; import javax.swing.JLabel;
import javax.swing.JList; import javax.swing.JList;
public class DefaultFancyListCellRenderer extends AbstractFancyListCellRenderer { public class DefaultFancyListCellRenderer extends AbstractFancyListCellRenderer {
private final JLabel label = new DefaultListCellRenderer(); private final JLabel label = new DefaultListCellRenderer();
public DefaultFancyListCellRenderer() { public DefaultFancyListCellRenderer() {
add(label); add(label);
} }
public DefaultFancyListCellRenderer(int padding) { public DefaultFancyListCellRenderer(int padding) {
super(new Insets(padding, padding, padding, padding)); super(new Insets(padding, padding, padding, padding));
add(label); add(label);
} }
public DefaultFancyListCellRenderer(Insets padding) { public DefaultFancyListCellRenderer(Insets padding) {
super(padding); super(padding);
add(label); add(label);
} }
protected DefaultFancyListCellRenderer(int padding, int margin, Color selectedBorderColor) { protected DefaultFancyListCellRenderer(int padding, int margin, Color selectedBorderColor) {
super(new Insets(padding, padding, padding, padding), new Insets(margin, margin, margin, margin), selectedBorderColor); super(new Insets(padding, padding, padding, padding), new Insets(margin, margin, margin, margin), selectedBorderColor);
add(label); add(label);
} }
@Override @Override
protected void configureListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { protected void configureListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
super.configureListCellRendererComponent(list, value, index, isSelected, cellHasFocus); super.configureListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
label.setOpaque(false); label.setOpaque(false);
setText(String.valueOf(value)); label.setText(String.valueOf(value));
} }
public void setIcon(Icon icon) { public void setIcon(Icon icon) {
label.setIcon(icon); label.setIcon(icon);
} }
public void setText(String text) { public void setText(String text) {
label.setText(text); label.setText(text);
} }
public void setHorizontalTextPosition(int textPosition) { public void setHorizontalTextPosition(int textPosition) {
label.setHorizontalTextPosition(textPosition); label.setHorizontalTextPosition(textPosition);
} }
public void setVerticalTextPosition(int textPosition) { public void setVerticalTextPosition(int textPosition) {
label.setVerticalTextPosition(textPosition); label.setVerticalTextPosition(textPosition);
} }
@Override @Override
public void setForeground(Color fg) { public void setForeground(Color fg) {
super.setForeground(fg); super.setForeground(fg);

View File

@ -0,0 +1,53 @@
package net.filebot.util.ui;
import javax.swing.JList;
import javax.swing.ListModel;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
public class PrototypeCellValueUpdater<T> implements ListDataListener {
private int longestItemLength = -1;
private JList<T> list;
private T defaultValue;
public PrototypeCellValueUpdater(JList<T> list, T defaultValue) {
this.list = list;
this.defaultValue = defaultValue;
}
@Override
public void intervalRemoved(ListDataEvent evt) {
// reset prototype value
ListModel<T> m = (ListModel<T>) evt.getSource();
if (m.getSize() == 0) {
longestItemLength = -1;
list.setPrototypeCellValue(null);
}
}
@Override
public void intervalAdded(ListDataEvent evt) {
contentsChanged(evt);
}
@Override
public void contentsChanged(ListDataEvent evt) {
ListModel<T> m = (ListModel<T>) evt.getSource();
for (int i = evt.getIndex0(); i <= evt.getIndex1() && i < m.getSize(); i++) {
T item = m.getElementAt(i);
int itemLength = item.toString().length();
if (itemLength > longestItemLength) {
// cell values will not be updated if the prototype object remains the same (even if the object has changed) so we need to reset it
if (item == list.getPrototypeCellValue()) {
list.setPrototypeCellValue(defaultValue);
}
longestItemLength = itemLength;
list.setPrototypeCellValue(item);
}
}
}
}