mirror of
https://github.com/mitb-archive/filebot
synced 2025-03-10 06:20:27 -04:00
Refactor ConflictDialog and make it more user-friendly
This commit is contained in:
parent
a029bdf94c
commit
541a9cd18a
@ -12,8 +12,6 @@ import java.awt.Dimension;
|
||||
import java.awt.Window;
|
||||
import java.awt.event.ActionEvent;
|
||||
import java.awt.event.KeyEvent;
|
||||
import java.awt.event.MouseAdapter;
|
||||
import java.awt.event.MouseEvent;
|
||||
import java.io.File;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
@ -23,50 +21,36 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.swing.Icon;
|
||||
import javax.swing.JButton;
|
||||
import javax.swing.JComponent;
|
||||
import javax.swing.JDialog;
|
||||
import javax.swing.JList;
|
||||
import javax.swing.JScrollPane;
|
||||
import javax.swing.JTable;
|
||||
import javax.swing.KeyStroke;
|
||||
import javax.swing.ListSelectionModel;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.table.AbstractTableModel;
|
||||
import javax.swing.table.DefaultTableCellRenderer;
|
||||
|
||||
import ca.odell.glazedlists.BasicEventList;
|
||||
import ca.odell.glazedlists.EventList;
|
||||
import ca.odell.glazedlists.swing.DefaultEventListModel;
|
||||
import net.filebot.ResourceManager;
|
||||
import net.filebot.UserFiles;
|
||||
import net.filebot.media.VideoQuality;
|
||||
import net.filebot.util.ui.DefaultFancyListCellRenderer;
|
||||
import net.miginfocom.swing.MigLayout;
|
||||
|
||||
class ConflictDialog extends JDialog {
|
||||
|
||||
private ConflictTableModel model = new ConflictTableModel();
|
||||
private EventList<Conflict> model = new BasicEventList<Conflict>();
|
||||
private boolean cancel = true;
|
||||
|
||||
public ConflictDialog(Window owner, List<Conflict> conflicts) {
|
||||
super(owner, "Conflicts", ModalityType.DOCUMENT_MODAL);
|
||||
model.addAll(conflicts);
|
||||
|
||||
model.setData(conflicts);
|
||||
|
||||
JTable table = new JTable(model);
|
||||
table.setDefaultRenderer(File.class, new FileRenderer());
|
||||
table.setDefaultRenderer(Conflict.class, new ConflictRenderer());
|
||||
table.setFillsViewportHeight(true);
|
||||
table.setAutoCreateRowSorter(true);
|
||||
table.setAutoCreateColumnsFromModel(true);
|
||||
table.setAutoResizeMode(JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS);
|
||||
|
||||
table.setRowSelectionAllowed(true);
|
||||
table.setColumnSelectionAllowed(false);
|
||||
table.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
|
||||
table.getColumnModel().getColumn(0).setMaxWidth(40);
|
||||
table.setRowHeight(25);
|
||||
table.setPreferredScrollableViewportSize(new Dimension(500, 250));
|
||||
|
||||
table.addMouseListener(new OpenListener());
|
||||
JList<Conflict> list = new JList<Conflict>(new DefaultEventListModel<Conflict>(model));
|
||||
list.setCellRenderer(new ConflictCellRenderer());
|
||||
list.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
|
||||
|
||||
// force white background (e.g. GTK LaF default table background is gray)
|
||||
setBackground(getPanelBackground());
|
||||
@ -74,22 +58,32 @@ class ConflictDialog extends JDialog {
|
||||
JComponent c = (JComponent) getContentPane();
|
||||
c.setLayout(new MigLayout("insets dialog, nogrid, fill", "", "[fill][pref!]"));
|
||||
|
||||
c.add(new JScrollPane(table), "grow, wrap");
|
||||
c.add(new JScrollPane(list), "grow, wrap");
|
||||
c.add(newButton("Cancel", ResourceManager.getIcon("dialog.cancel"), this::cancel), "tag left");
|
||||
c.add(newButton("Continue", ResourceManager.getIcon("dialog.continue"), this::ok), "tag ok");
|
||||
|
||||
JButton b = newButton("Override", ResourceManager.getIcon("dialog.continue.invalid"), this::override);
|
||||
b.setEnabled(conflicts.stream().anyMatch(it -> it.override));
|
||||
b.addActionListener(evt -> b.setEnabled(false));
|
||||
c.add(b, "tag next");
|
||||
|
||||
// focus "Continue" button
|
||||
SwingUtilities.invokeLater(c.getComponent(2)::requestFocusInWindow);
|
||||
JButton overrideButton = newButton("Override", ResourceManager.getIcon("dialog.continue.invalid"), this::override);
|
||||
overrideButton.setEnabled(conflicts.stream().anyMatch(it -> it.override));
|
||||
overrideButton.addActionListener(evt -> overrideButton.setEnabled(false));
|
||||
c.add(overrideButton, "tag next");
|
||||
|
||||
installAction(c, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), newAction("Cancel", this::cancel));
|
||||
|
||||
list.addMouseListener(mouseClicked(evt -> {
|
||||
if (evt.getClickCount() == 2) {
|
||||
Conflict selection = list.getSelectedValue();
|
||||
if (selection != null) {
|
||||
List<File> files = Stream.of(selection.source, selection.destination).filter(File::exists).distinct().collect(toList());
|
||||
UserFiles.revealFiles(files);
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
// give default focus to "Continue" button
|
||||
SwingUtilities.invokeLater(c.getComponent(2)::requestFocusInWindow);
|
||||
|
||||
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
|
||||
setMinimumSize(new Dimension(640, 280));
|
||||
setMinimumSize(new Dimension(340, 280));
|
||||
pack();
|
||||
}
|
||||
|
||||
@ -98,12 +92,12 @@ class ConflictDialog extends JDialog {
|
||||
}
|
||||
|
||||
public List<Conflict> getConflicts() {
|
||||
return model.getData();
|
||||
return model;
|
||||
}
|
||||
|
||||
private void override(ActionEvent evt) {
|
||||
// delete existing destination files and create new data model
|
||||
List<Conflict> data = model.getData().stream().map(c -> {
|
||||
List<Conflict> data = model.stream().map(c -> {
|
||||
// safety check
|
||||
if (!c.override) {
|
||||
return c;
|
||||
@ -120,7 +114,8 @@ class ConflictDialog extends JDialog {
|
||||
}).filter(Objects::nonNull).collect(toList());
|
||||
|
||||
// insert new conflict data
|
||||
model.setData(data);
|
||||
model.clear();
|
||||
model.addAll(data);
|
||||
|
||||
// continue if there are no more conflicts
|
||||
if (data.isEmpty()) {
|
||||
@ -138,8 +133,35 @@ class ConflictDialog extends JDialog {
|
||||
setVisible(false);
|
||||
}
|
||||
|
||||
private static class ConflictCellRenderer extends DefaultFancyListCellRenderer {
|
||||
|
||||
@Override
|
||||
protected void configureListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
|
||||
setHighlightingEnabled(false);
|
||||
|
||||
Conflict conflict = (Conflict) value;
|
||||
super.configureListCellRendererComponent(list, conflict.getDetails(), index, isSelected, cellHasFocus);
|
||||
setIcon(ResourceManager.getIcon("status.warning"));
|
||||
setToolTipText(formatToolTip(conflict));
|
||||
}
|
||||
|
||||
private String formatToolTip(Conflict conflict) {
|
||||
StringBuilder html = new StringBuilder(64).append("<html>");
|
||||
appendTooltipParagraph(html, "Conflicts", conflict.issues.keySet().stream().map(Objects::toString).collect(joining(" | ")));
|
||||
appendTooltipParagraph(html, "Source", conflict.source.getPath());
|
||||
appendTooltipParagraph(html, "Destination", conflict.destination.getPath());
|
||||
return html.append("</html>").toString();
|
||||
}
|
||||
|
||||
private StringBuilder appendTooltipParagraph(StringBuilder html, String label, Object value) {
|
||||
return html.append("<p style='width:350px; margin:3px'><b>").append(label).append(":</b><br>").append(escapeHTML(value.toString())).append("</p>");
|
||||
}
|
||||
}
|
||||
|
||||
public static class Conflict {
|
||||
|
||||
public static final Comparator<Conflict> SEVERITY_ORDER = Comparator.comparingInt(Conflict::getSeverity).thenComparing(Conflict::getDetails);
|
||||
|
||||
public final File source;
|
||||
public final File destination;
|
||||
|
||||
@ -153,6 +175,14 @@ class ConflictDialog extends JDialog {
|
||||
this.override = override;
|
||||
}
|
||||
|
||||
public String getDetails() {
|
||||
return issues.values().iterator().next();
|
||||
}
|
||||
|
||||
public int getSeverity() {
|
||||
return -issues.keySet().iterator().next().ordinal();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return issues.toString();
|
||||
@ -160,7 +190,7 @@ class ConflictDialog extends JDialog {
|
||||
|
||||
public enum Kind {
|
||||
|
||||
OVERRIDE_FAILED, FILE_EXISTS, MISSING_EXTENSION, DUPLICATE, OVERLAP;
|
||||
MISSING_EXTENSION, OVERLAP, DUPLICATE, FILE_EXISTS, OVERRIDE_FAILED;
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
@ -181,140 +211,6 @@ class ConflictDialog extends JDialog {
|
||||
}
|
||||
}
|
||||
|
||||
private static class ConflictTableModel extends AbstractTableModel {
|
||||
|
||||
private Conflict[] data = new Conflict[0];
|
||||
|
||||
public void setData(List<Conflict> data) {
|
||||
this.data = data.toArray(new Conflict[0]);
|
||||
|
||||
// update table
|
||||
fireTableDataChanged();
|
||||
}
|
||||
|
||||
private List<Conflict> getData() {
|
||||
return unmodifiableList(asList(data));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColumnName(int column) {
|
||||
switch (column) {
|
||||
case 0:
|
||||
return "";
|
||||
case 1:
|
||||
return "Issue";
|
||||
case 2:
|
||||
return "Source";
|
||||
case 3:
|
||||
return "Destination";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getColumnCount() {
|
||||
return 2;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getRowCount() {
|
||||
return data.length;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Class<?> getColumnClass(int column) {
|
||||
switch (column) {
|
||||
case 0:
|
||||
return Icon.class;
|
||||
case 1:
|
||||
return Conflict.class;
|
||||
case 2:
|
||||
return File.class;
|
||||
case 3:
|
||||
return File.class;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getValueAt(int row, int column) {
|
||||
Conflict conflict = data[row];
|
||||
|
||||
switch (column) {
|
||||
case 0:
|
||||
return ResourceManager.getIcon(conflict.issues.isEmpty() ? "status.ok" : "status.warning");
|
||||
case 1:
|
||||
return conflict;
|
||||
case 2:
|
||||
return conflict.source;
|
||||
case 3:
|
||||
return conflict.destination;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static class FileRenderer extends DefaultTableCellRenderer {
|
||||
|
||||
@Override
|
||||
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
|
||||
if (value == null) {
|
||||
return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
|
||||
}
|
||||
|
||||
File f = (File) value;
|
||||
super.getTableCellRendererComponent(table, f.getName(), isSelected, hasFocus, row, column);
|
||||
setToolTipText(f.getPath());
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
private static class ConflictRenderer extends DefaultTableCellRenderer {
|
||||
|
||||
@Override
|
||||
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
|
||||
if (value == null) {
|
||||
return super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
|
||||
}
|
||||
|
||||
Conflict conflict = (Conflict) value;
|
||||
String label = conflict.issues.isEmpty() ? "OK" : conflict.issues.values().iterator().next();
|
||||
super.getTableCellRendererComponent(table, label, isSelected, hasFocus, row, column);
|
||||
setToolTipText(formatToolTip(conflict));
|
||||
return this;
|
||||
}
|
||||
|
||||
private String formatToolTip(Conflict conflict) {
|
||||
StringBuilder html = new StringBuilder(64).append("<html>");
|
||||
conflict.issues.forEach((k, v) -> appendTooltipParagraph(html, k.toString(), v.toString()));
|
||||
appendTooltipParagraph(html, "Source", conflict.source.getPath());
|
||||
appendTooltipParagraph(html, "Destination", conflict.destination.getPath());
|
||||
return html.append("</html>").toString();
|
||||
}
|
||||
|
||||
private StringBuilder appendTooltipParagraph(StringBuilder html, String label, Object value) {
|
||||
return html.append("<p style='width:350px; margin:3px'><b>").append(label).append(":</b><br>").append(escapeHTML(value.toString())).append("</p>");
|
||||
}
|
||||
}
|
||||
|
||||
private static class OpenListener extends MouseAdapter {
|
||||
|
||||
@Override
|
||||
public void mouseClicked(MouseEvent evt) {
|
||||
if (evt.getClickCount() == 2) {
|
||||
JTable table = (JTable) evt.getSource();
|
||||
int row = table.getSelectedRow();
|
||||
if (row >= 0) {
|
||||
ConflictTableModel m = (ConflictTableModel) table.getModel();
|
||||
Conflict c = m.getData().get(row);
|
||||
|
||||
List<File> files = Stream.of(c.source, c.destination).filter(File::exists).distinct().collect(toList());
|
||||
UserFiles.revealFiles(files);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Map<File, List<File>> getSourceFilesForDestination(Map<File, File> renameMap, Comparator<File> order) {
|
||||
Map<File, List<File>> duplicates = renameMap.entrySet().stream().collect(groupingBy(e -> {
|
||||
return resolve(e.getKey(), e.getValue());
|
||||
@ -326,6 +222,26 @@ class ConflictDialog extends JDialog {
|
||||
return duplicates;
|
||||
}
|
||||
|
||||
private static String formatDetails(String line1, String line2, Object... args) {
|
||||
StringBuilder html = new StringBuilder(64).append("<html><p style='padding:3px 6px'>");
|
||||
html.append(String.format(String.join("<br>", line1, line2), stream(args).map(a -> {
|
||||
if (a instanceof File) {
|
||||
File file = (File) a;
|
||||
return file.getName();
|
||||
}
|
||||
if (a instanceof File[]) {
|
||||
File[] files = (File[]) a;
|
||||
return stream(files).map(File::getName).collect(joining(" | ", "[", "]"));
|
||||
}
|
||||
return String.valueOf(a);
|
||||
}).map(s -> formatDetailsVariable(s)).toArray()));
|
||||
return html.append("</p></html>").toString();
|
||||
}
|
||||
|
||||
private static String formatDetailsVariable(String value) {
|
||||
return "<nobr><span style='color:#32D515'>" + value + "</span></nobr>";
|
||||
}
|
||||
|
||||
public static boolean check(Component parent, Map<File, File> renameMap) {
|
||||
List<Conflict> conflicts = new ArrayList<Conflict>();
|
||||
|
||||
@ -340,27 +256,27 @@ class ConflictDialog extends JDialog {
|
||||
|
||||
// output files must have a valid file extension
|
||||
if (getExtension(to) == null && from.isFile()) {
|
||||
String details = String.format("Destination file path [%s] has no file extension. Fix via Edit Format.", to.getName());
|
||||
String details = formatDetails("Destination file path [%s] has no file extension.", "Fix via Edit Format.", to);
|
||||
issues.put(Conflict.Kind.MISSING_EXTENSION, details);
|
||||
}
|
||||
|
||||
// one file per unique output path
|
||||
List<File> duplicates = sourceFilesForDestination.get(to);
|
||||
File chosen = duplicates.get(0);
|
||||
if (duplicates.size() > 1 && !from.equals(chosen)) {
|
||||
String details = String.format("Multiple source files map to the same destination file %s ➔ [%s]. The highest-quality file [%s] was chosen instead of [%s].", duplicates.stream().map(File::getName).collect(toList()), to.getName(), chosen.getName(), from.getName());
|
||||
File[] duplicates = sourceFilesForDestination.get(to).toArray(new File[0]);
|
||||
File chosen = duplicates[0];
|
||||
if (duplicates.length > 1 && !from.equals(chosen)) {
|
||||
String details = formatDetails("Multiple source files map to the same destination file %s ➔ %s.", "The highest-quality file %s was chosen instead of %s.", duplicates, to, chosen, from);
|
||||
issues.put(Conflict.Kind.DUPLICATE, details);
|
||||
}
|
||||
|
||||
// check if input and output overlap
|
||||
if (renameMap.containsKey(to) && !to.equals(from)) {
|
||||
String details = String.format("Overlapping file mapping between [%s ➔ %s] and [%s ➔ %s]. Fix via Edit Format.", from.getName(), to.getName(), to.getName(), renameMap.get(to).getName());
|
||||
String details = formatDetails("Overlapping file mapping between %s ➔ %s and %s ➔ %s.", "Fix via Edit Format.", from, to, to, renameMap.get(to));
|
||||
issues.put(Conflict.Kind.OVERLAP, details);
|
||||
}
|
||||
|
||||
// check if destination path already exists
|
||||
if (to.exists() && !to.equals(from)) {
|
||||
String details = String.format("Destination file path [%s] already exists. Fix via Override.", to.getName());
|
||||
String details = formatDetails("Destination file path %s already exists.", "Fix via Override.", to.getName());
|
||||
issues.put(Conflict.Kind.FILE_EXISTS, details);
|
||||
}
|
||||
|
||||
@ -375,6 +291,8 @@ class ConflictDialog extends JDialog {
|
||||
return true;
|
||||
}
|
||||
|
||||
conflicts.sort(Conflict.SEVERITY_ORDER);
|
||||
|
||||
ConflictDialog dialog = new ConflictDialog(getWindow(parent), conflicts);
|
||||
dialog.setLocation(getOffsetLocation(dialog.getOwner()));
|
||||
dialog.setVisible(true);
|
||||
|
Loading…
x
Reference in New Issue
Block a user