diff --git a/source/net/sourceforge/filebot/resources/action.report.png b/source/net/sourceforge/filebot/resources/action.report.png new file mode 100644 index 00000000..779ad58e Binary files /dev/null and b/source/net/sourceforge/filebot/resources/action.report.png differ diff --git a/source/net/sourceforge/filebot/resources/edit.clear.png b/source/net/sourceforge/filebot/resources/edit.clear.png new file mode 100644 index 00000000..e6c8e8b9 Binary files /dev/null and b/source/net/sourceforge/filebot/resources/edit.clear.png differ diff --git a/source/net/sourceforge/filebot/resources/status.link.broken.png b/source/net/sourceforge/filebot/resources/status.link.broken.png new file mode 100644 index 00000000..52357530 Binary files /dev/null and b/source/net/sourceforge/filebot/resources/status.link.broken.png differ diff --git a/source/net/sourceforge/filebot/resources/status.link.ok.png b/source/net/sourceforge/filebot/resources/status.link.ok.png new file mode 100644 index 00000000..ae8cae80 Binary files /dev/null and b/source/net/sourceforge/filebot/resources/status.link.ok.png differ diff --git a/source/net/sourceforge/filebot/ui/MainFrame.java b/source/net/sourceforge/filebot/ui/MainFrame.java index c180340c..1d8c6778 100644 --- a/source/net/sourceforge/filebot/ui/MainFrame.java +++ b/source/net/sourceforge/filebot/ui/MainFrame.java @@ -2,8 +2,7 @@ package net.sourceforge.filebot.ui; -import static javax.swing.ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER; -import static javax.swing.ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER; +import static javax.swing.ScrollPaneConstants.*; import java.awt.Color; import java.awt.FlowLayout; @@ -12,9 +11,7 @@ import java.awt.dnd.DropTargetAdapter; import java.awt.dnd.DropTargetDragEvent; import java.awt.dnd.DropTargetDropEvent; import java.awt.dnd.DropTargetEvent; -import java.util.ArrayList; import java.util.Arrays; -import java.util.List; import javax.swing.JComponent; import javax.swing.JFrame; @@ -47,7 +44,7 @@ import net.sourceforge.tuned.ui.TunedUtilities; public class MainFrame extends JFrame { - private JList selectionList = new PanelSelectionList(); + private JList selectionList = new PanelSelectionList(createPanelBuilders()); private HeaderPanel headerPanel = new HeaderPanel(); @@ -99,21 +96,19 @@ public class MainFrame extends JFrame { } - protected List createPanelBuilders() { - List builders = new ArrayList(); - - builders.add(new ListPanelBuilder()); - builders.add(new RenamePanelBuilder()); - builders.add(new AnalyzePanelBuilder()); - builders.add(new EpisodeListPanelBuilder()); - builders.add(new SubtitlePanelBuilder()); - builders.add(new SfvPanelBuilder()); - - return builders; + protected PanelBuilder[] createPanelBuilders() { + return new PanelBuilder[] { + new ListPanelBuilder(), + new RenamePanelBuilder(), + new AnalyzePanelBuilder(), + new EpisodeListPanelBuilder(), + new SubtitlePanelBuilder(), + new SfvPanelBuilder() + }; } - protected void showPanel(final PanelBuilder selectedBuilder) { + protected void showPanel(PanelBuilder selectedBuilder) { final JComponent contentPane = (JComponent) getContentPane(); JComponent panel = null; @@ -148,7 +143,7 @@ public class MainFrame extends JFrame { private static final int SELECTDELAY_ON_DRAG_OVER = 300; - public PanelSelectionList(PanelBuilder... builders) { + public PanelSelectionList(PanelBuilder[] builders) { super(builders); setCellRenderer(new PanelCellRenderer()); diff --git a/source/net/sourceforge/filebot/ui/panel/rename/History.java b/source/net/sourceforge/filebot/ui/panel/rename/History.java index ffbceb4a..d4a87ad8 100644 --- a/source/net/sourceforge/filebot/ui/panel/rename/History.java +++ b/source/net/sourceforge/filebot/ui/panel/rename/History.java @@ -2,8 +2,12 @@ package net.sourceforge.filebot.ui.panel.rename; +import static java.util.Collections.*; + import java.io.File; +import java.io.IOException; import java.util.ArrayList; +import java.util.Collection; import java.util.Date; import java.util.List; import java.util.Map.Entry; @@ -21,7 +25,17 @@ import javax.xml.bind.annotation.XmlRootElement; class History { @XmlElement(name = "sequence") - private List sequences = new ArrayList(); + private List sequences; + + + public History() { + this.sequences = new ArrayList(); + } + + + public History(Collection sequences) { + this.sequences = new ArrayList(sequences); + } public static class Sequence { @@ -44,7 +58,18 @@ class History { public List elements() { - return elements; + return unmodifiableList(elements); + } + + + @Override + public boolean equals(Object obj) { + if (obj instanceof Sequence) { + Sequence other = (Sequence) obj; + return date.equals(other.date) && elements.equals(other.elements); + } + + return false; } } @@ -71,19 +96,30 @@ class History { } - public File from() { - return new File(dir, from); + public String from() { + return from; } - public File to() { - return new File(dir, to); + public String to() { + return to; + } + + + @Override + public boolean equals(Object obj) { + if (obj instanceof Element) { + Element element = (Element) obj; + return to.equals(element.to) && from.equals(element.from) && dir.getPath().equals(element.dir.getPath()); + } + + return false; } } public List sequences() { - return sequences; + return unmodifiableList(sequences); } @@ -111,17 +147,26 @@ class History { sequence.elements.add(element); } - sequences.add(sequence); + add(sequence); } - public void add(History other) { - this.sequences.addAll(other.sequences); + public void add(Sequence sequence) { + this.sequences.add(sequence); } - public int size() { - return sequences.size(); + public void addAll(Collection sequences) { + this.sequences.addAll(sequences); + } + + + public void merge(History history) { + for (Sequence sequence : history.sequences()) { + if (!sequences.contains(sequence)) { + add(sequence); + } + } } @@ -130,20 +175,36 @@ class History { } - public void store(File file) throws JAXBException { - Marshaller marshaller = JAXBContext.newInstance(History.class).createMarshaller(); - marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); + @Override + public boolean equals(Object obj) { + if (obj instanceof History) { + History other = (History) obj; + return sequences.equals(other.sequences); + } - marshaller.marshal(this, file); + return false; } - public void load(File file) throws JAXBException { - Unmarshaller unmarshaller = JAXBContext.newInstance(History.class).createUnmarshaller(); - - History history = ((History) unmarshaller.unmarshal(file)); - - clear(); - add(history); + public static void exportHistory(History history, File file) throws IOException { + try { + Marshaller marshaller = JAXBContext.newInstance(History.class).createMarshaller(); + marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, Boolean.TRUE); + + marshaller.marshal(history, file); + } catch (JAXBException e) { + throw new IOException(e); + } + } + + + public static History importHistory(File file) throws IOException { + try { + Unmarshaller unmarshaller = JAXBContext.newInstance(History.class).createUnmarshaller(); + + return ((History) unmarshaller.unmarshal(file)); + } catch (JAXBException e) { + throw new IOException(e); + } } } diff --git a/source/net/sourceforge/filebot/ui/panel/rename/HistoryDialog.java b/source/net/sourceforge/filebot/ui/panel/rename/HistoryDialog.java new file mode 100644 index 00000000..31a6bc37 --- /dev/null +++ b/source/net/sourceforge/filebot/ui/panel/rename/HistoryDialog.java @@ -0,0 +1,846 @@ + +package net.sourceforge.filebot.ui.panel.rename; + + +import static java.awt.Font.*; +import static java.util.Collections.*; +import static javax.swing.JOptionPane.*; + +import java.awt.Color; +import java.awt.Component; +import java.awt.Dimension; +import java.awt.Window; +import java.awt.event.ActionEvent; +import java.awt.event.MouseAdapter; +import java.awt.event.MouseEvent; +import java.awt.event.MouseListener; +import java.io.File; +import java.io.IOException; +import java.text.DateFormat; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Date; +import java.util.EnumSet; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Map.Entry; + +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.DefaultListCellRenderer; +import javax.swing.JButton; +import javax.swing.JComponent; +import javax.swing.JDialog; +import javax.swing.JFileChooser; +import javax.swing.JLabel; +import javax.swing.JList; +import javax.swing.JOptionPane; +import javax.swing.JPanel; +import javax.swing.JPopupMenu; +import javax.swing.JScrollPane; +import javax.swing.JTable; +import javax.swing.JTextField; +import javax.swing.ListSelectionModel; +import javax.swing.RowFilter; +import javax.swing.SortOrder; +import javax.swing.RowSorter.SortKey; +import javax.swing.border.CompoundBorder; +import javax.swing.border.TitledBorder; +import javax.swing.event.DocumentEvent; +import javax.swing.event.ListSelectionEvent; +import javax.swing.event.ListSelectionListener; +import javax.swing.table.AbstractTableModel; +import javax.swing.table.DefaultTableCellRenderer; +import javax.swing.table.DefaultTableColumnModel; +import javax.swing.table.TableModel; +import javax.swing.table.TableRowSorter; + +import net.miginfocom.swing.MigLayout; +import net.sourceforge.filebot.ResourceManager; +import net.sourceforge.filebot.ui.panel.rename.History.Element; +import net.sourceforge.filebot.ui.panel.rename.History.Sequence; +import net.sourceforge.filebot.ui.transfer.FileExportHandler; +import net.sourceforge.filebot.ui.transfer.FileTransferablePolicy; +import net.sourceforge.filebot.ui.transfer.LoadAction; +import net.sourceforge.filebot.ui.transfer.SaveAction; +import net.sourceforge.filebot.ui.transfer.TransferablePolicy.TransferAction; +import net.sourceforge.tuned.FileUtilities; +import net.sourceforge.tuned.FileUtilities.ExtensionFileFilter; +import net.sourceforge.tuned.ui.GradientStyle; +import net.sourceforge.tuned.ui.LazyDocumentListener; +import net.sourceforge.tuned.ui.notification.SeparatorBorder; +import net.sourceforge.tuned.ui.notification.SeparatorBorder.Position; + + +class HistoryDialog extends JDialog { + + private final JLabel infoLabel = new JLabel(); + + private final JTextField filterEditor = new JTextField(); + + private final SequenceTableModel sequenceModel = new SequenceTableModel(); + + private final ElementTableModel elementModel = new ElementTableModel(); + + private final JTable sequenceTable = createTable(sequenceModel); + + private final JTable elementTable = createTable(elementModel); + + + public HistoryDialog(Window owner) { + super(owner, "Rename History", ModalityType.DOCUMENT_MODAL); + + // bold title label in header + JLabel title = new JLabel(this.getTitle()); + title.setFont(title.getFont().deriveFont(BOLD)); + + JPanel header = new JPanel(new MigLayout("insets dialog, nogrid, fillx")); + + header.setBackground(Color.white); + header.setBorder(new SeparatorBorder(1, new Color(0xB4B4B4), new Color(0xACACAC), GradientStyle.LEFT_TO_RIGHT, Position.BOTTOM)); + + header.add(title, "wrap"); + header.add(infoLabel, "gap indent*2, wrap paragraph:push"); + + JPanel content = new JPanel(new MigLayout("fill, insets dialog, nogrid", "", "[pref!][150px:pref:200px][200px:pref:max, grow][pref!]")); + + content.add(new JLabel("Filter:"), "gap indent:push"); + content.add(filterEditor, "wmin 120px, gap rel"); + content.add(new JButton(clearFilterAction), "w 24px!, h 24px!, gap right indent, wrap"); + + content.add(createScrollPaneGroup("Sequences", sequenceTable), "growx, wrap paragraph"); + content.add(createScrollPaneGroup("Elements", elementTable), "growx, wrap paragraph"); + + // use ADD by default + Action importAction = new LoadAction("Import", null, importHandler) { + + @Override + public TransferAction getTransferAction(ActionEvent evt) { + // if SHIFT was pressed when the button was clicked, assume PUT action, use ADD by default + return ((evt.getModifiers() & ActionEvent.SHIFT_MASK) != 0) ? TransferAction.PUT : TransferAction.ADD; + } + }; + + content.add(new JButton(importAction), "wmin button, hmin 25px, gap indent, sg button"); + content.add(new JButton(new SaveAction("Export", null, exportHandler)), "gap rel, sg button"); + content.add(new JButton(closeAction), "gap left unrel:push, gap right indent, sg button"); + + JComponent pane = (JComponent) getContentPane(); + pane.setLayout(new MigLayout("fill, insets 0, nogrid")); + + pane.add(header, "hmin 60px, growx, dock north"); + pane.add(content, "grow"); + + // initialize selection modes + sequenceTable.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); + elementTable.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + + // bind element model to selected sequence + sequenceTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() { + + @Override + public void valueChanged(ListSelectionEvent e) { + if (e.getValueIsAdjusting()) + return; + + if (sequenceTable.getSelectedRow() >= 0) { + int index = sequenceTable.convertRowIndexToModel(sequenceTable.getSelectedRow()); + + elementModel.setData(sequenceModel.getRow(index).elements()); + } + } + }); + + // clear sequence selection when elements are selected + elementTable.getSelectionModel().addListSelectionListener(new ListSelectionListener() { + + @Override + public void valueChanged(ListSelectionEvent e) { + if (elementTable.getSelectedRow() >= 0) { + // allow selected rows only in one of the two tables + sequenceTable.getSelectionModel().clearSelection(); + } + } + }); + + // sort by number descending + sequenceTable.getRowSorter().setSortKeys(singletonList(new SortKey(0, SortOrder.DESCENDING))); + + // change date format + sequenceTable.setDefaultRenderer(Date.class, new DefaultTableCellRenderer() { + + private final DateFormat format = DateFormat.getDateTimeInstance(DateFormat.MEDIUM, DateFormat.SHORT); + + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { + return super.getTableCellRendererComponent(table, format.format(value), isSelected, hasFocus, row, column); + } + }); + + // display broken status in second column + elementTable.setDefaultRenderer(String.class, new DefaultTableCellRenderer() { + + @Override + public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { + super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column); + + // reset icon + setIcon(null); + + if (column == 1) { + if (elementModel.isBroken(table.convertRowIndexToModel(row))) { + setIcon(ResourceManager.getIcon("status.link.broken")); + } else { + setIcon(ResourceManager.getIcon("status.link.ok")); + } + } + + return this; + } + }); + + // update sequence and element filter on change + filterEditor.getDocument().addDocumentListener(new LazyDocumentListener() { + + @Override + public void update(DocumentEvent e) { + List filterList = new ArrayList(); + + // filter by all words + for (String word : filterEditor.getText().split("\\s+")) { + filterList.add(new HistoryFilter(word)); + } + + // use filter on both tables + for (JTable table : Arrays.asList(sequenceTable, elementTable)) { + TableRowSorter sorter = (TableRowSorter) table.getRowSorter(); + sorter.setRowFilter(RowFilter.andFilter(filterList)); + } + + if (sequenceTable.getSelectedRow() < 0 && sequenceTable.getRowCount() > 0) { + // selection lost, maybe due to filtering, auto-select next row + sequenceTable.getSelectionModel().addSelectionInterval(0, 0); + } + } + }); + + // install context menu + sequenceTable.addMouseListener(contextMenuProvider); + elementTable.addMouseListener(contextMenuProvider); + + // initialize window properties + setDefaultCloseOperation(DISPOSE_ON_CLOSE); + setLocationByPlatform(true); + setResizable(true); + setSize(580, 640); + } + + + public void setModel(History history) { + // update table model + sequenceModel.setData(history.sequences()); + + if (sequenceTable.getRowCount() > 0) { + // auto-select first element and update element table + sequenceTable.getSelectionModel().addSelectionInterval(0, 0); + } else { + // clear element table + elementModel.setData(new ArrayList(0)); + } + + // display basic statistics + initializeInfoLabel(); + } + + + public History getModel() { + return new History(sequenceModel.getData()); + } + + + public JLabel getInfoLabel() { + return infoLabel; + } + + + private void initializeInfoLabel() { + int count = 0; + Date since = new Date(); + + for (Sequence sequence : sequenceModel.getData()) { + count += sequence.elements().size(); + + if (sequence.date().before(since)) + since = sequence.date(); + } + + infoLabel.setText(String.format("A total of %,d files have been renamed since %s.", count, DateFormat.getDateInstance().format(since))); + } + + + private JScrollPane createScrollPaneGroup(String title, JComponent component) { + JScrollPane scrollPane = new JScrollPane(component); + scrollPane.setBorder(new CompoundBorder(new TitledBorder(title), scrollPane.getBorder())); + + return scrollPane; + } + + + private JTable createTable(TableModel model) { + JTable table = new JTable(model); + table.setAutoCreateRowSorter(true); + table.setFillsViewportHeight(true); + + // hide grid + table.setShowGrid(false); + table.setIntercellSpacing(new Dimension(0, 0)); + + // decrease column width for the row number columns + DefaultTableColumnModel m = ((DefaultTableColumnModel) table.getColumnModel()); + m.getColumn(0).setMaxWidth(50); + + return table; + } + + private final Action closeAction = new AbstractAction("Close") { + + @Override + public void actionPerformed(ActionEvent e) { + setVisible(false); + dispose(); + } + }; + + private final Action clearFilterAction = new AbstractAction(null, ResourceManager.getIcon("edit.clear")) { + + @Override + public void actionPerformed(ActionEvent e) { + filterEditor.setText(""); + } + }; + + private final MouseListener contextMenuProvider = new MouseAdapter() { + + @Override + public void mousePressed(MouseEvent e) { + maybeShowPopup(e); + } + + + @Override + public void mouseReleased(MouseEvent e) { + maybeShowPopup(e); + } + + + private void maybeShowPopup(MouseEvent e) { + if (e.isPopupTrigger()) { + JTable table = (JTable) e.getSource(); + + int clickedRow = table.rowAtPoint(e.getPoint()); + + if (clickedRow < 0) { + // no row was clicked + return; + } + + if (!table.getSelectionModel().isSelectedIndex(clickedRow)) { + // if clicked row is not selected, set selection to this row (and deselect all other currently selected row) + table.getSelectionModel().setSelectionInterval(clickedRow, clickedRow); + } + + List selection = new ArrayList(); + + for (int i : table.getSelectedRows()) { + int index = table.convertRowIndexToModel(i); + + if (sequenceModel == table.getModel()) { + selection.addAll(sequenceModel.getRow(index).elements()); + } else if (elementModel == table.getModel()) { + selection.add(elementModel.getRow(index)); + } + } + + if (selection.size() > 0) { + JPopupMenu menu = new JPopupMenu(); + menu.add(new RevertAction(selection, HistoryDialog.this)); + + // display popup + menu.show(table, e.getX(), e.getY()); + } + } + } + }; + + + private static class RevertAction extends AbstractAction { + + public static final String ELEMENTS = "elements"; + public static final String PARENT = "parent"; + + + public RevertAction(Collection elements, HistoryDialog parent) { + putValue(NAME, "Revert..."); + putValue(ELEMENTS, elements.toArray(new Element[0])); + putValue(PARENT, parent); + } + + + public Element[] elements() { + return (Element[]) getValue(ELEMENTS); + } + + + public HistoryDialog parent() { + return (HistoryDialog) getValue(PARENT); + } + + + private enum Option { + Rename { + @Override + public String toString() { + return "Rename"; + } + }, + ChangeDirectory { + @Override + public String toString() { + return "Change Directory"; + } + }, + Cancel { + @Override + public String toString() { + return "Cancel"; + } + } + } + + + @Override + public void actionPerformed(ActionEvent e) { + // use default directory + File directory = null; + + Option selectedOption = Option.ChangeDirectory; + + // change directory option + while (selectedOption == Option.ChangeDirectory) { + List missingFiles = getMissingFiles(directory); + + Object message; + int type; + Set