package net.filebot.ui.subtitle;
import static java.nio.charset.StandardCharsets.*;
import static net.filebot.Logging.*;
import static net.filebot.MediaTypes.*;
import static net.filebot.UserFiles.*;
import static net.filebot.subtitle.SubtitleUtilities.*;
import static net.filebot.util.FileUtilities.*;
import static net.filebot.util.ui.SwingUI.*;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.io.File;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.CancellationException;
import java.util.logging.Level;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.ListModel;
import javax.swing.SwingUtilities;
import javax.swing.border.LineBorder;
import ca.odell.glazedlists.BasicEventList;
import ca.odell.glazedlists.EventList;
import ca.odell.glazedlists.FilterList;
import ca.odell.glazedlists.GlazedLists;
import ca.odell.glazedlists.ListSelection;
import ca.odell.glazedlists.ObservableElementList;
import ca.odell.glazedlists.SortedList;
import ca.odell.glazedlists.TextFilterator;
import ca.odell.glazedlists.matchers.MatcherEditor;
import ca.odell.glazedlists.swing.DefaultEventListModel;
import ca.odell.glazedlists.swing.DefaultEventSelectionModel;
import ca.odell.glazedlists.swing.TextComponentMatcherEditor;
import net.filebot.ResourceManager;
import net.filebot.Settings;
import net.filebot.subtitle.SubtitleFormat;
import net.filebot.ui.subtitle.SubtitlePackage.Download.Phase;
import net.filebot.ui.transfer.DefaultTransferHandler;
import net.filebot.util.ExceptionUtilities;
import net.filebot.util.ui.ListView;
import net.filebot.util.ui.SwingUI;
import net.filebot.vfs.MemoryFile;
import net.miginfocom.swing.MigLayout;
class SubtitleDownloadComponent extends JComponent {
private EventList<SubtitlePackage> packages = new BasicEventList<SubtitlePackage>();
private EventList<MemoryFile> files = new BasicEventList<MemoryFile>();
private SubtitlePackageCellRenderer renderer = new SubtitlePackageCellRenderer();
private JTextField filterEditor = new JTextField();
public SubtitleDownloadComponent() {
final JList packageList = new JList(createPackageListModel());
packageList.setFixedCellHeight(32);
packageList.setCellRenderer(renderer);
// better selection behaviour
DefaultEventSelectionModel<SubtitlePackage> packageSelection = new DefaultEventSelectionModel<SubtitlePackage>(packages);
packageSelection.setSelectionMode(ListSelection.MULTIPLE_INTERVAL_SELECTION_DEFENSIVE);
packageList.setSelectionModel(packageSelection);
// context menu and fetch on double click
packageList.addMouseListener(packageListMouseHandler);
// file list view
final JList fileList = new ListView(createFileListModel()) {
@Override
protected String convertValueToText(Object value) {
MemoryFile file = (MemoryFile) value;
return file.getName();
}
@Override
protected Icon convertValueToIcon(Object value) {
if (SUBTITLE_FILES.accept(value.toString()))
return ResourceManager.getIcon("file.subtitle");
return ResourceManager.getIcon("file.generic");
}
};
// better selection behaviour
DefaultEventSelectionModel<MemoryFile> fileSelection = new DefaultEventSelectionModel<MemoryFile>(files);
fileSelection.setSelectionMode(ListSelection.MULTIPLE_INTERVAL_SELECTION_DEFENSIVE);
fileList.setSelectionModel(fileSelection);
// install dnd and clipboard export handler
MemoryFileListExportHandler memoryFileExportHandler = new MemoryFileListExportHandler();
fileList.setTransferHandler(new DefaultTransferHandler(null, memoryFileExportHandler, memoryFileExportHandler));
fileList.setDragEnabled(true);
fileList.addMouseListener(fileListMouseHandler);
JButton clearButton = createImageButton(clearFilterAction);
clearButton.setOpaque(false);
setLayout(new MigLayout("nogrid, fill, novisualpadding", "[fill]", "[pref!][fill]"));
add(new JLabel("Filter:"), "gap indent:push");
add(filterEditor, "wmin 120px, gap rel");
add(clearButton, "w pref!, h pref!");
add(new JScrollPane(packageList), "newline, hmin 80px");
JScrollPane scrollPane = new JScrollPane(fileList);
scrollPane.setViewportBorder(new LineBorder(fileList.getBackground()));
add(scrollPane, "newline, hmin max(80px, 30%)");
// install fetch action
SwingUI.installAction(packageList, KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), new AbstractAction("Fetch") {
@Override
public void actionPerformed(ActionEvent e) {
fetch(packageList.getSelectedValuesList().toArray());
}
});
// install open action
SwingUI.installAction(fileList, KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), new AbstractAction("Open") {
@Override
public void actionPerformed(ActionEvent e) {
open(fileList.getSelectedValuesList().toArray());
}
});
}
protected ListModel createPackageListModel() {
// allow filtering by language name and subtitle name
MatcherEditor<SubtitlePackage> matcherEditor = new TextComponentMatcherEditor<SubtitlePackage>(filterEditor, new TextFilterator<SubtitlePackage>() {
@Override
public void getFilterStrings(List<String> list, SubtitlePackage element) {
list.add(element.getLanguage().getName());
list.add(element.getName());
}
});
// source list
EventList<SubtitlePackage> source = getPackageModel();
// filter list
source = new FilterList<SubtitlePackage>(source, matcherEditor);
// listen to changes (e.g. download progress)
source = new ObservableElementList<SubtitlePackage>(source, GlazedLists.beanConnector(SubtitlePackage.class));
// as list model
return new DefaultEventListModel<SubtitlePackage>(source);
}
protected ListModel createFileListModel() {
// source list
EventList<MemoryFile> source = getFileModel();
// sort by name
source = new SortedList<MemoryFile>(source, new Comparator<MemoryFile>() {
@Override
public int compare(MemoryFile m1, MemoryFile m2) {
return m1.getName().compareToIgnoreCase(m2.getName());
}
});
// as list model
return new DefaultEventListModel<MemoryFile>(source);
}
public void reset() {
// cancel and reset download workers
for (SubtitlePackage subtitle : packages) {
subtitle.reset();
}
files.clear();
}
public EventList<SubtitlePackage> getPackageModel() {
return packages;
}
public EventList<MemoryFile> getFileModel() {
return files;
}
public void setLanguageVisible(boolean visible) {
renderer.getLanguageLabel().setVisible(visible);
}
private void fetch(Object[] selection) {
for (Object value : selection) {
fetch((SubtitlePackage) value);
}
}
private void fetch(final SubtitlePackage subtitle) {
if (subtitle.getDownload().isStarted()) {
// download has been started already
return;
}
// listen to download
subtitle.addPropertyChangeListener(new PropertyChangeListener() {
@Override
public void propertyChange(PropertyChangeEvent evt) {
if (evt.getNewValue() == Phase.DONE) {
try {
files.addAll(subtitle.getDownload().get());
} catch (CancellationException e) {
// ignore cancellation
} catch (Exception e) {
log.log(Level.WARNING, ExceptionUtilities.getRootCauseMessage(e), e);
// reset download
subtitle.reset();
}
// listener no longer required
subtitle.removePropertyChangeListener(this);
}
}
});
// enqueue worker
subtitle.getDownload().start();
}
private void open(Object[] selection) {
try {
for (Object object : selection) {
MemoryFile file = (MemoryFile) object;
// only open subtitle files
if (SUBTITLE_FILES.accept(file.getName())) {
open(file);
}
}
} catch (Exception e) {
log.log(Level.WARNING, e.getMessage(), e);
}
}
private void open(MemoryFile file) throws IOException {
SubtitleViewer viewer = new SubtitleViewer(file.getName());
viewer.getTitleLabel().setText("Subtitle Viewer");
viewer.getInfoLabel().setText(file.getPath());
viewer.setData(decodeSubtitles(file));
viewer.setVisible(true);
}
private void save(Object[] selection) {
try {
for (Object object : selection) {
MemoryFile data = (MemoryFile) object;
File destination = showSaveDialogSelectFile(false, new File(validateFileName(data.getName())), "Save Subtitles as ...", new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "Save"));
if (destination != null) {
writeFile(data.getData(), destination);
}
}
} catch (Exception e) {
log.log(Level.WARNING, e.getMessage(), e);
}
}
private void export(Object[] selection) {
try {
File selectedOutputFolder = null;
// default values
SubtitleFormat selectedFormat = SubtitleFormat.SubRip;
long selectedTimingOffset = 0;
Charset selectedEncoding = UTF_8;
// just use default values when we can't use a JFC with accessory component (also Swing OSX LaF doesn't seem to support JFileChooser::setAccessory)
if (Settings.isMacApp()) {
// COCOA || AWT
selectedOutputFolder = showOpenDialogSelectFolder(null, "Export Subtitles to Folder (SubRip / UTF-8)", new ActionEvent(this, ActionEvent.ACTION_PERFORMED, "Export"));
} else {
// Swing
SubtitleFileChooser sfc = new SubtitleFileChooser();
sfc.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
if (sfc.showSaveDialog(getWindow(this)) == JFileChooser.APPROVE_OPTION) {
selectedOutputFolder = sfc.getSelectedFile();
selectedFormat = sfc.getSelectedFormat();
selectedTimingOffset = sfc.getTimingOffset();
selectedEncoding = sfc.getSelectedEncoding();
}
}
if (selectedOutputFolder != null) {
List<File> outputFiles = new ArrayList<File>();
for (Object object : selection) {
MemoryFile file = (MemoryFile) object;
// normalize name and auto-adjust extension
String name = validateFileName(getNameWithoutExtension(file.getName()));
File destination = new File(selectedOutputFolder, name + "." + selectedFormat.getFilter().extension());
SubtitleFormat targetFormat = selectedFormat.getFilter().accept(file.getName()) ? null : selectedFormat; // check if format conversion is necessary
writeFile(exportSubtitles(file, targetFormat, selectedTimingOffset, selectedEncoding), destination);
outputFiles.add(destination);
}
// reveal exported files
revealFiles(outputFiles);
}
} catch (Exception e) {
log.log(Level.WARNING, e.getMessage(), e);
}
}
private final Action clearFilterAction = new AbstractAction("Clear Filter", ResourceManager.getIcon("edit.clear")) {
@Override
public void actionPerformed(ActionEvent e) {
filterEditor.setText("");
}
};
private final MouseListener packageListMouseHandler = new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
// fetch on double click
if (SwingUtilities.isLeftMouseButton(e) && (e.getClickCount() == 2)) {
JList list = (JList) e.getSource();
fetch(list.getSelectedValuesList().toArray());
}
}
@Override
public void mousePressed(MouseEvent e) {
maybeShowPopup(e);
}
@Override
public void mouseReleased(MouseEvent e) {
maybeShowPopup(e);
}
private void maybeShowPopup(MouseEvent e) {
if (e.isPopupTrigger()) {
JList list = (JList) e.getSource();
int index = list.locationToIndex(e.getPoint());
if (index >= 0 && !list.isSelectedIndex(index)) {
// auto-select clicked element
list.setSelectedIndex(index);
}
final Object[] selection = list.getSelectedValuesList().toArray();
if (selection.length > 0) {
JPopupMenu contextMenu = new JPopupMenu();
JMenuItem item = contextMenu.add(new AbstractAction("Download", ResourceManager.getIcon("package.fetch")) {
@Override
public void actionPerformed(ActionEvent e) {
fetch(selection);
}
});
// disable menu item if all selected elements have been fetched already
item.setEnabled(isPending(selection));
contextMenu.show(e.getComponent(), e.getX(), e.getY());
}
}
}
private boolean isPending(Object[] selection) {
for (Object value : selection) {
SubtitlePackage subtitle = (SubtitlePackage) value;
if (!subtitle.getDownload().isStarted()) {
// pending download found
return true;
}
}
return false;
}
};
private final MouseListener fileListMouseHandler = new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
// open on double click
if (SwingUtilities.isLeftMouseButton(e) && (e.getClickCount() == 2)) {
JList list = (JList) e.getSource();
// open selection
open(list.getSelectedValuesList().toArray());
}
}
@Override
public void mousePressed(MouseEvent e) {
maybeShowPopup(e);
}
@Override
public void mouseReleased(MouseEvent e) {
maybeShowPopup(e);
}
private void maybeShowPopup(MouseEvent e) {
if (e.isPopupTrigger()) {
JList list = (JList) e.getSource();
int index = list.locationToIndex(e.getPoint());
if (index >= 0 && !list.isSelectedIndex(index)) {
// auto-select clicked element
list.setSelectedIndex(index);
}
Object[] selection = list.getSelectedValuesList().toArray();
if (selection.length > 0) {
JPopupMenu contextMenu = new JPopupMenu();
contextMenu.add(newAction("Preview", ResourceManager.getIcon("action.find"), evt -> open(selection))); // Open
contextMenu.add(newAction("Save As...", ResourceManager.getIcon("action.save"), evt -> save(selection))); // Save As...
contextMenu.add(newAction("Export...", ResourceManager.getIcon("action.export"), evt -> export(selection))); // Export...
contextMenu.show(e.getComponent(), e.getX(), e.getY());
}
}
}
};
}