From ef71e2fff8eef3f4821720b09abc226a05ae4234 Mon Sep 17 00:00:00 2001 From: Reinhard Pointner Date: Sun, 20 Mar 2016 18:33:31 +0000 Subject: [PATCH] Rewrite ListPanel for parallel editing and testing of format expressions --- source/net/filebot/cli/CmdlineOperations.java | 12 +- .../net/filebot/format/MediaBindingBean.java | 12 +- source/net/filebot/torrent/Torrent.java | 48 +--- .../filebot/ui/FileBotListExportHandler.java | 24 +- .../ui/episodelist/EpisodeListPanel.java | 2 +- .../ui/list/FileListTransferablePolicy.java | 75 ++++-- .../filebot/ui/list/IndexedBindingBean.java | 50 ++++ source/net/filebot/ui/list/ListItem.java | 46 ++++ source/net/filebot/ui/list/ListPanel.java | 227 ++++++++++++------ .../net/filebot/ui/rename/FormatDialog.java | 8 +- .../rename/NamesListTransferablePolicy.java | 5 +- source/net/filebot/ui/rename/RenameList.java | 42 +--- .../ui/transfer/TextFileExportHandler.java | 14 +- source/net/filebot/util/EntryList.java | 68 +++--- source/net/filebot/util/FunctionList.java | 27 +++ .../util/ui/DefaultFancyListCellRenderer.java | 14 +- .../util/ui/PrototypeCellValueUpdater.java | 53 ++++ 17 files changed, 474 insertions(+), 253 deletions(-) create mode 100644 source/net/filebot/ui/list/IndexedBindingBean.java create mode 100644 source/net/filebot/ui/list/ListItem.java create mode 100644 source/net/filebot/util/FunctionList.java create mode 100644 source/net/filebot/util/ui/PrototypeCellValueUpdater.java diff --git a/source/net/filebot/cli/CmdlineOperations.java b/source/net/filebot/cli/CmdlineOperations.java index f644a84f..2583850e 100644 --- a/source/net/filebot/cli/CmdlineOperations.java +++ b/source/net/filebot/cli/CmdlineOperations.java @@ -202,7 +202,7 @@ public class CmdlineOperations implements CmdlineInterface { } // fetch episode data - Collection episodes = fetchEpisodeSet(db, seriesNames, sortOrder, locale, strict); + List episodes = fetchEpisodeSet(db, seriesNames, sortOrder, locale, strict); if (episodes.size() == 0) { log.warning("Failed to fetch episode data: " + seriesNames); continue; @@ -277,7 +277,7 @@ public class CmdlineOperations implements CmdlineInterface { return validMatches; } - private Set fetchEpisodeSet(final EpisodeListProvider db, final Collection names, final SortOrder sortOrder, final Locale locale, final boolean strict) throws Exception { + private List fetchEpisodeSet(final EpisodeListProvider db, final Collection names, final SortOrder sortOrder, final Locale locale, final boolean strict) throws Exception { Set shows = new LinkedHashSet(); Set episodes = new LinkedHashSet(); @@ -304,7 +304,7 @@ public class CmdlineOperations implements CmdlineInterface { } } - return episodes; + return new ArrayList(episodes); } public List renameMovie(Collection 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 if (movie == null) { log.fine(format("Auto-detect movie from context: [%s]", file)); - Collection options = detectMovie(file, service, locale, strict); + List options = detectMovie(file, service, locale, strict); // apply filter if defined options = applyExpressionFilter(options, filter); @@ -871,13 +871,13 @@ public class CmdlineOperations implements CmdlineInterface { return destination; } - private List applyExpressionFilter(Collection input, ExpressionFilter filter) throws Exception { + private List applyExpressionFilter(List input, ExpressionFilter filter) throws Exception { if (filter == null) { return new ArrayList(input); } log.fine(format("Apply Filter: {%s}", filter.getExpression())); - Map context = new EntryList(null, input); + Map context = new EntryList(null, input); List output = new ArrayList(input.size()); for (T it : input) { if (filter.matches(new MediaBindingBean(it, null, context))) { diff --git a/source/net/filebot/format/MediaBindingBean.java b/source/net/filebot/format/MediaBindingBean.java index 8832eed6..1f0133f6 100644 --- a/source/net/filebot/format/MediaBindingBean.java +++ b/source/net/filebot/format/MediaBindingBean.java @@ -67,7 +67,7 @@ public class MediaBindingBean { private final Object infoObject; private final File mediaFile; - private final Map context; + private final Map context; private MediaInfo mediaInfo; private Object metaInfo; @@ -76,7 +76,7 @@ public class MediaBindingBean { this(infoObject, mediaFile, singletonMap(mediaFile, infoObject)); } - public MediaBindingBean(Object infoObject, File mediaFile, Map context) { + public MediaBindingBean(Object infoObject, File mediaFile, Map context) { this.infoObject = infoObject; this.mediaFile = mediaFile; this.context = context; @@ -903,14 +903,14 @@ public class MediaBindingBean { @Define("model") public List getModel() { List result = new ArrayList(); - for (Entry it : context.entrySet()) { + for (Entry it : context.entrySet()) { result.add(createBindingObject(it.getKey(), it.getValue(), context)); } return result; } @Define("json") - public String getInfoObjectDump() throws Exception { + public String getInfoObjectDump() { 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()))) { // prefer equal match from current context if possible if (context != null) { - for (Entry it : context.entrySet()) { + for (Entry it : context.entrySet()) { if (infoObject.equals(it.getValue()) && VIDEO_FILES.accept(it.getKey())) { return it.getKey(); } @@ -994,7 +994,7 @@ public class MediaBindingBean { return undefined(String.format("%s[%d][%s]", streamKind, streamNumber, join(keys, ", "))); } - private AssociativeScriptObject createBindingObject(File file, Object info, Map context) { + private AssociativeScriptObject createBindingObject(File file, Object info, Map context) { MediaBindingBean mediaBindingBean = new MediaBindingBean(info, file, context) { @Override @Define(undefined) diff --git a/source/net/filebot/torrent/Torrent.java b/source/net/filebot/torrent/Torrent.java index 5d970e6c..1a0af4f1 100644 --- a/source/net/filebot/torrent/Torrent.java +++ b/source/net/filebot/torrent/Torrent.java @@ -1,6 +1,7 @@ package net.filebot.torrent; import static java.nio.charset.StandardCharsets.*; +import static java.util.Collections.*; import java.io.BufferedInputStream; import java.io.File; @@ -9,11 +10,13 @@ import java.io.IOException; import java.io.InputStream; import java.nio.charset.Charset; import java.util.ArrayList; -import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.Map; +import net.filebot.vfs.FileInfo; +import net.filebot.vfs.SimpleFileInfo; + public class Torrent { private String name; @@ -24,7 +27,7 @@ public class Torrent { private Long creationDate; private Long pieceLength; - private List files; + private List files; private boolean singleFileTorrent; protected Torrent() { @@ -59,7 +62,7 @@ public class Torrent { // torrent contains multiple entries singleFileTorrent = false; - List entries = new ArrayList(); + List entries = new ArrayList(); for (Object fileMapObject : (List) infoMap.get("files")) { Map fileMap = (Map) fileMapObject; @@ -81,17 +84,17 @@ public class Torrent { 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 { // single file torrent singleFileTorrent = true; 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; } - public List getFiles() { + public List getFiles() { return files; } @@ -155,35 +158,4 @@ public class Torrent { 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(); - } - } - } diff --git a/source/net/filebot/ui/FileBotListExportHandler.java b/source/net/filebot/ui/FileBotListExportHandler.java index b03cd87b..5230f5db 100644 --- a/source/net/filebot/ui/FileBotListExportHandler.java +++ b/source/net/filebot/ui/FileBotListExportHandler.java @@ -1,35 +1,39 @@ package net.filebot.ui; - +import java.awt.Cursor; import java.io.PrintWriter; import net.filebot.ui.transfer.TextFileExportHandler; +public class FileBotListExportHandler extends TextFileExportHandler { -public class FileBotListExportHandler extends TextFileExportHandler { + protected final FileBotList list; - protected final FileBotList list; - - - public FileBotListExportHandler(FileBotList list) { + public FileBotListExportHandler(FileBotList list) { this.list = list; } - @Override public boolean canExport() { return list.getModel().size() > 0; } - @Override public void export(PrintWriter out) { - for (Object entry : list.getModel()) { - out.println(entry); + try { + 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 public String getDefaultFileName() { diff --git a/source/net/filebot/ui/episodelist/EpisodeListPanel.java b/source/net/filebot/ui/episodelist/EpisodeListPanel.java index 49af6d6a..63456ea0 100644 --- a/source/net/filebot/ui/episodelist/EpisodeListPanel.java +++ b/source/net/filebot/ui/episodelist/EpisodeListPanel.java @@ -278,7 +278,7 @@ public class EpisodeListPanel extends AbstractSearchPanel implements ClipboardHandler { public EpisodeListExportHandler(FileBotList list) { super(list); diff --git a/source/net/filebot/ui/list/FileListTransferablePolicy.java b/source/net/filebot/ui/list/FileListTransferablePolicy.java index e121214e..1174789a 100644 --- a/source/net/filebot/ui/list/FileListTransferablePolicy.java +++ b/source/net/filebot/ui/list/FileListTransferablePolicy.java @@ -1,25 +1,60 @@ 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.ui.transfer.FileTransferable.*; import static net.filebot.util.FileUtilities.*; +import java.awt.datatransfer.DataFlavor; +import java.awt.datatransfer.Transferable; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.function.Consumer; 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.util.FileUtilities; import net.filebot.util.FileUtilities.ExtensionFileFilter; +import net.filebot.web.Episode; class FileListTransferablePolicy extends FileTransferablePolicy { - private FileBotList list; + private static final DataFlavor episodeArrayFlavor = ArrayTransferable.flavor(Episode.class); - public FileListTransferablePolicy(FileBotList list) { - this.list = list; + private Consumer title; + private Consumer format; + private Consumer> model; + + public FileListTransferablePolicy(Consumer title, Consumer format, Consumer> 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 @@ -29,48 +64,44 @@ class FileListTransferablePolicy extends FileTransferablePolicy { @Override protected void clear() { - list.getModel().clear(); + format.accept(""); + title.accept(""); + model.accept(emptyList()); } @Override protected void load(List files, TransferAction action) throws IOException { // set title based on parent folder of first file - list.setTitle(FileUtilities.getFolderName(files.get(0).getParentFile())); - - // clear selection - list.getListComponent().clearSelection(); + title.accept(getFolderName(files.get(0).getParentFile())); if (containsOnly(files, TORRENT_FILES)) { loadTorrents(files); } else { // if only one folder was dropped, use its name as title 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 - for (File file : listFiles(files)) { - list.getModel().add(FileUtilities.getName(file)); - } + format.accept(ListPanel.DEFAULT_FILE_FORMAT); + model.accept(listFiles(files)); } } private void loadTorrents(List files) throws IOException { List torrents = new ArrayList(files.size()); - for (File file : files) { torrents.add(new Torrent(file)); } - if (torrents.size() == 1) { - list.setTitle(FileUtilities.getNameWithoutExtension(torrents.get(0).getName())); + // set title + if (torrents.size() > 0) { + title.accept(getNameWithoutExtension(torrents.get(0).getName())); } - for (Torrent torrent : torrents) { - for (Torrent.Entry entry : torrent.getFiles()) { - list.getModel().add(FileUtilities.getNameWithoutExtension(entry.getName())); - } - } + // add torrent entries + format.accept(ListPanel.DEFAULT_FILE_FORMAT); + model.accept(torrents.stream().flatMap(t -> t.getFiles().stream()).collect(toList())); } @Override diff --git a/source/net/filebot/ui/list/IndexedBindingBean.java b/source/net/filebot/ui/list/IndexedBindingBean.java new file mode 100644 index 00000000..3e7976e4 --- /dev/null +++ b/source/net/filebot/ui/list/IndexedBindingBean.java @@ -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 getContext(List context) { + return new EntryList(new FunctionList((List) context, IndexedBindingBean::getMediaFile), context); + } + +} diff --git a/source/net/filebot/ui/list/ListItem.java b/source/net/filebot/ui/list/ListItem.java new file mode 100644 index 00000000..eb1c5199 --- /dev/null +++ b/source/net/filebot/ui/list/ListItem.java @@ -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(); + } + +} diff --git a/source/net/filebot/ui/list/ListPanel.java b/source/net/filebot/ui/list/ListPanel.java index b13127cc..1f17599d 100644 --- a/source/net/filebot/ui/list/ListPanel.java +++ b/source/net/filebot/ui/list/ListPanel.java @@ -1,34 +1,38 @@ package net.filebot.ui.list; import static java.awt.Font.*; -import static java.lang.Math.*; -import static net.filebot.Logging.*; -import static net.filebot.media.MediaDetection.*; +import static java.util.stream.Collectors.*; +import static javax.swing.BorderFactory.*; import static net.filebot.util.ui.SwingUI.*; import java.awt.BorderLayout; +import java.awt.Color; import java.awt.Font; import java.awt.datatransfer.Transferable; -import java.awt.event.ActionEvent; -import java.awt.event.KeyEvent; -import java.text.NumberFormat; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; +import java.io.PrintWriter; 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.SimpleBindings; +import javax.script.ScriptException; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JLabel; +import javax.swing.JList; import javax.swing.JPanel; import javax.swing.JSpinner; import javax.swing.JSpinner.NumberEditor; import javax.swing.JTextField; -import javax.swing.KeyStroke; 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; @@ -40,42 +44,92 @@ import net.filebot.ui.transfer.LoadAction; import net.filebot.ui.transfer.SaveAction; import net.filebot.ui.transfer.TransferablePolicy; 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; public class ListPanel extends JComponent { - private FileBotList list = new FileBotList(); + 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 toSpinnerModel = new SpinnerNumberModel(20, 0, Integer.MAX_VALUE, 1); + private FileBotList list = new FileBotList(); + public ListPanel() { list.setTitle("Title"); - textField.setFont(new Font(MONOSPACED, PLAIN, 11)); - - list.setTransferablePolicy(new FileListTransferablePolicy(list)); - list.setExportHandler(new FileBotListExportHandler(list)); + // need a fixed cell size for high performance scrolling + list.getListComponent().setFixedCellHeight(28); + list.getListComponent().getModel().addListDataListener(new PrototypeCellValueUpdater(list.getListComponent(), "")); list.getRemoveAction().setEnabled(true); + list.setTransferablePolicy(new FileListTransferablePolicy(list::setTitle, editor::setText, this::createItemSequence)); + list.setExportHandler(new FileBotListExportHandler(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 toSpinner = new JSpinner(toSpinnerModel); fromSpinner.setEditor(new NumberEditor(fromSpinner, "#")); 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]")); - add(new JLabel("Pattern:"), "gapbefore indent"); - add(textField, "gap related, wmin 2cm, sizegroupy editor"); + JLabel patternLabel = new JLabel("Pattern:"); + add(patternLabel, "gapbefore indent"); + add(editorScrollPane, "gap related, growx, wmin 2cm, h pref!, sizegroupy editor"); 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(toSpinner, "gap related, wmax 14mm, sizegroup spinner, sizegroupy editor"); - add(newButton("Create", ResourceManager.getIcon("action.export"), this::create), "gap 7mm, gapafter indent, wrap paragraph"); + add(toSpinner, "gap related, wmax 15mm, sizegroup spinner, sizegroupy editor"); + add(newButton("Sequence", ResourceManager.getIcon("action.export"), evt -> createItemSequence()), "gap 7mm, gapafter indent, wrap paragraph"); add(list, "grow"); @@ -86,57 +140,92 @@ public class ListPanel extends JComponent { 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) { - // clear selection - list.getListComponent().clearSelection(); + private RSyntaxTextArea createEditor() { + RSyntaxTextArea editor = new RSyntaxTextArea(new RSyntaxDocument(SyntaxConstants.SYNTAX_STYLE_GROOVY) { + @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 itr = list.getModel().listIterator(); itr.hasNext();) { + itr.set(new ListItem(itr.next().getBindings(), format)); + } + } + + public void createItemSequence(List objects) { + List 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 to = toSpinnerModel.getNumber().intValue(); - try { - ExpressionFormat format = new ExpressionFormat(textField.getText()); + List context = IntStream.rangeClosed(from, to).boxed().collect(toList()); + List 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 names = new ArrayList(); - - 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 title = getSeriesNameMatcher(true).matchAll((names.size() < 5 ? names : names.subList(0, 4)).toArray(new String[0])); - - list.setTitle(title.isEmpty() ? "List" : title.iterator().next()); - - list.getModel().clear(); - list.getModel().addAll(names); - } catch (Exception e) { - log.log(Level.WARNING, ExceptionUtilities.getMessage(e), e); - } + editor.setText(DEFAULT_SEQUENCE_FORMAT); + list.setTitle("Sequence"); + list.getListComponent().clearSelection(); + list.getModel().clear(); + list.getModel().addAll(items); } @Subscribe diff --git a/source/net/filebot/ui/rename/FormatDialog.java b/source/net/filebot/ui/rename/FormatDialog.java index 3ab79393..adfc171c 100644 --- a/source/net/filebot/ui/rename/FormatDialog.java +++ b/source/net/filebot/ui/rename/FormatDialog.java @@ -50,9 +50,9 @@ import javax.swing.JLabel; import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; +import javax.swing.JTextField; import javax.swing.SwingWorker; import javax.swing.Timer; -import javax.swing.border.EmptyBorder; import javax.swing.event.DocumentEvent; import javax.swing.event.PopupMenuEvent; import javax.swing.event.PopupMenuListener; @@ -206,11 +206,11 @@ public class FormatDialog extends JDialog { editorScrollPane.setVerticalScrollBarPolicy(RTextScrollPane.VERTICAL_SCROLLBAR_NEVER); editorScrollPane.setHorizontalScrollBarPolicy(RTextScrollPane.HORIZONTAL_SCROLLBAR_NEVER); - editorScrollPane.setViewportBorder(new EmptyBorder(7, 0, 7, 0)); - editorScrollPane.setBackground(editor.getBackground()); + editorScrollPane.setViewportBorder(createEmptyBorder(7, 2, 7, 2)); 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(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"); diff --git a/source/net/filebot/ui/rename/NamesListTransferablePolicy.java b/source/net/filebot/ui/rename/NamesListTransferablePolicy.java index a30c49e7..3cdacff7 100644 --- a/source/net/filebot/ui/rename/NamesListTransferablePolicy.java +++ b/source/net/filebot/ui/rename/NamesListTransferablePolicy.java @@ -145,10 +145,7 @@ class NamesListTransferablePolicy extends FileTransferablePolicy { protected void loadTorrentFiles(List files, List values) throws IOException { for (File file : files) { Torrent torrent = new Torrent(file); - - for (Torrent.Entry entry : torrent.getFiles()) { - values.add(new SimpleFileInfo(entry.getPath(), entry.getLength())); - } + values.addAll(torrent.getFiles()); } } diff --git a/source/net/filebot/ui/rename/RenameList.java b/source/net/filebot/ui/rename/RenameList.java index 8273a5aa..0b71a656 100644 --- a/source/net/filebot/ui/rename/RenameList.java +++ b/source/net/filebot/ui/rename/RenameList.java @@ -11,16 +11,14 @@ import java.awt.event.MouseEvent; import javax.swing.Action; import javax.swing.JButton; import javax.swing.JPanel; -import javax.swing.ListModel; import javax.swing.ListSelectionModel; -import javax.swing.event.ListDataEvent; -import javax.swing.event.ListDataListener; import ca.odell.glazedlists.EventList; import net.filebot.ResourceManager; import net.filebot.ui.FileBotList; import net.filebot.ui.transfer.LoadAction; import net.filebot.util.ui.ActionPopup; +import net.filebot.util.ui.PrototypeCellValueUpdater; import net.miginfocom.swing.MigLayout; class RenameList extends FileBotList { @@ -36,43 +34,7 @@ class RenameList extends FileBotList { // need a fixed cell size for high performance scrolling list.setFixedCellHeight(28); - list.getModel().addListDataListener(new ListDataListener() { - - 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.getModel().addListDataListener(new PrototypeCellValueUpdater(list, "")); list.addMouseListener(dndReorderMouseAdapter); list.addMouseMotionListener(dndReorderMouseAdapter); diff --git a/source/net/filebot/ui/transfer/TextFileExportHandler.java b/source/net/filebot/ui/transfer/TextFileExportHandler.java index 078a03f1..21c4178e 100644 --- a/source/net/filebot/ui/transfer/TextFileExportHandler.java +++ b/source/net/filebot/ui/transfer/TextFileExportHandler.java @@ -1,7 +1,6 @@ package net.filebot.ui.transfer; - import java.awt.datatransfer.Transferable; import java.io.File; import java.io.IOException; @@ -11,38 +10,28 @@ import java.io.StringWriter; import javax.swing.JComponent; import javax.swing.TransferHandler; - public abstract class TextFileExportHandler implements TransferableExportHandler, FileExportHandler { @Override public abstract boolean canExport(); - public abstract void export(PrintWriter out); - @Override public abstract String getDefaultFileName(); - @Override public void export(File file) throws IOException { - PrintWriter out = new PrintWriter(file, "UTF-8"); - - try { + try (PrintWriter out = new PrintWriter(file, "UTF-8")) { export(out); - } finally { - out.close(); } } - @Override public int getSourceActions(JComponent c) { return canExport() ? TransferHandler.COPY_OR_MOVE : TransferHandler.NONE; } - @Override public Transferable createTransferable(JComponent c) { // get transfer data @@ -52,7 +41,6 @@ public abstract class TextFileExportHandler implements TransferableExportHandler return new TextFileTransferable(getDefaultFileName(), buffer.toString()); } - @Override public void exportDone(JComponent source, Transferable data, int action) { diff --git a/source/net/filebot/util/EntryList.java b/source/net/filebot/util/EntryList.java index 8b462029..f66edaf2 100644 --- a/source/net/filebot/util/EntryList.java +++ b/source/net/filebot/util/EntryList.java @@ -2,27 +2,20 @@ package net.filebot.util; import static java.util.Collections.*; -import java.util.AbstractList; import java.util.AbstractMap; import java.util.AbstractSet; -import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Set; public class EntryList extends AbstractMap { - private final List> entryList = new ArrayList>(); + private List keys; + private List values; - public EntryList(Iterable keys, Iterable values) { - Iterator keySeq = keys != null ? keys.iterator() : emptyIterator(); - Iterator valueSeq = values != null ? values.iterator() : emptyIterator(); - - while (keySeq.hasNext() || valueSeq.hasNext()) { - K key = keySeq.hasNext() ? keySeq.next() : null; - V value = valueSeq.hasNext() ? valueSeq.next() : null; - entryList.add(new SimpleImmutableEntry(key, value)); - } + public EntryList(List keys, List values) { + this.keys = keys != null ? keys : emptyList(); + this.values = values != null ? values : emptyList(); } @Override @@ -31,35 +24,56 @@ public class EntryList extends AbstractMap { @Override public Iterator> iterator() { - return entryList.iterator(); + return new Iterator>() { + + private Iterator keySeq = keys.iterator(); + private Iterator valueSeq = values.iterator(); + + @Override + public boolean hasNext() { + return keySeq.hasNext() || valueSeq.hasNext(); + } + + @Override + public Entry next() { + K key = keySeq.hasNext() ? keySeq.next() : null; + V value = valueSeq.hasNext() ? valueSeq.next() : null; + return new SimpleImmutableEntry(key, value); + } + }; } @Override public int size() { - return entryList.size(); + return keys.size(); + } + }; + } + + @Override + public Set keySet() { + return new AbstractSet() { + + @Override + public Iterator iterator() { + return (Iterator) keys.iterator(); + } + + @Override + public int size() { + return keys.size(); } }; } @Override public List values() { - return new AbstractList() { - - @Override - public V get(int index) { - return entryList.get(index).getValue(); - } - - @Override - public int size() { - return entryList.size(); - } - }; + return (List) values; } @Override public int size() { - return entryList.size(); + return Math.max(keys.size(), values.size()); } } diff --git a/source/net/filebot/util/FunctionList.java b/source/net/filebot/util/FunctionList.java new file mode 100644 index 00000000..b7beeaf2 --- /dev/null +++ b/source/net/filebot/util/FunctionList.java @@ -0,0 +1,27 @@ +package net.filebot.util; + +import java.util.AbstractList; +import java.util.List; +import java.util.function.Function; + +public class FunctionList extends AbstractList { + + private List source; + private Function function; + + public FunctionList(List source, Function 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(); + } + +} diff --git a/source/net/filebot/util/ui/DefaultFancyListCellRenderer.java b/source/net/filebot/util/ui/DefaultFancyListCellRenderer.java index 6e5c1c1d..a5c89abd 100644 --- a/source/net/filebot/util/ui/DefaultFancyListCellRenderer.java +++ b/source/net/filebot/util/ui/DefaultFancyListCellRenderer.java @@ -1,7 +1,6 @@ package net.filebot.util.ui; - import java.awt.Color; import java.awt.Insets; @@ -10,63 +9,52 @@ import javax.swing.Icon; import javax.swing.JLabel; import javax.swing.JList; - public class DefaultFancyListCellRenderer extends AbstractFancyListCellRenderer { private final JLabel label = new DefaultListCellRenderer(); - public DefaultFancyListCellRenderer() { add(label); } - public DefaultFancyListCellRenderer(int padding) { super(new Insets(padding, padding, padding, padding)); add(label); } - public DefaultFancyListCellRenderer(Insets padding) { super(padding); add(label); } - protected DefaultFancyListCellRenderer(int padding, int margin, Color selectedBorderColor) { super(new Insets(padding, padding, padding, padding), new Insets(margin, margin, margin, margin), selectedBorderColor); add(label); } - @Override protected void configureListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { super.configureListCellRendererComponent(list, value, index, isSelected, cellHasFocus); label.setOpaque(false); - setText(String.valueOf(value)); + label.setText(String.valueOf(value)); } - public void setIcon(Icon icon) { label.setIcon(icon); } - public void setText(String text) { label.setText(text); } - public void setHorizontalTextPosition(int textPosition) { label.setHorizontalTextPosition(textPosition); } - public void setVerticalTextPosition(int textPosition) { label.setVerticalTextPosition(textPosition); } - @Override public void setForeground(Color fg) { super.setForeground(fg); diff --git a/source/net/filebot/util/ui/PrototypeCellValueUpdater.java b/source/net/filebot/util/ui/PrototypeCellValueUpdater.java new file mode 100644 index 00000000..e183166c --- /dev/null +++ b/source/net/filebot/util/ui/PrototypeCellValueUpdater.java @@ -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 implements ListDataListener { + + private int longestItemLength = -1; + + private JList list; + private T defaultValue; + + public PrototypeCellValueUpdater(JList list, T defaultValue) { + this.list = list; + this.defaultValue = defaultValue; + } + + @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++) { + 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); + } + } + } + +}