Improved conflict dialog
This commit is contained in:
parent
226559683b
commit
ab2aeee4f1
|
@ -0,0 +1,319 @@
|
||||||
|
package net.filebot.ui.rename;
|
||||||
|
|
||||||
|
import static java.util.Arrays.*;
|
||||||
|
import static java.util.Collections.*;
|
||||||
|
import static java.util.stream.Collectors.*;
|
||||||
|
import static net.filebot.util.FileUtilities.*;
|
||||||
|
import static net.filebot.util.ui.SwingUI.*;
|
||||||
|
|
||||||
|
import java.awt.Color;
|
||||||
|
import java.awt.Component;
|
||||||
|
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.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
|
import javax.swing.Icon;
|
||||||
|
import javax.swing.JComponent;
|
||||||
|
import javax.swing.JDialog;
|
||||||
|
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 net.filebot.ResourceManager;
|
||||||
|
import net.filebot.StandardRenameAction;
|
||||||
|
import net.filebot.UserFiles;
|
||||||
|
import net.miginfocom.swing.MigLayout;
|
||||||
|
|
||||||
|
class ConflictDialog extends JDialog {
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
SwingUtilities.invokeLater(() -> {
|
||||||
|
File s = new File(args[0]);
|
||||||
|
File d = new File(args[1]);
|
||||||
|
|
||||||
|
Map<File, File> map = new HashMap<>();
|
||||||
|
map.put(s, d);
|
||||||
|
|
||||||
|
Map<File, File> check = ConflictDialog.check(null, map);
|
||||||
|
System.out.println(check);
|
||||||
|
System.exit(0);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Map<File, File> check(Component parent, Map<File, File> renameMap) {
|
||||||
|
List<Conflict> conflicts = new ArrayList<Conflict>();
|
||||||
|
|
||||||
|
// sanity checks
|
||||||
|
Set<File> destFiles = new HashSet<File>();
|
||||||
|
|
||||||
|
renameMap.forEach((from, to) -> {
|
||||||
|
List<Object> issues = new ArrayList<Object>();
|
||||||
|
|
||||||
|
// resolve relative paths
|
||||||
|
to = resolve(from, to);
|
||||||
|
|
||||||
|
// output files must have a valid file extension
|
||||||
|
if (getExtension(to) == null && to.isFile()) {
|
||||||
|
issues.add("Missing extension");
|
||||||
|
}
|
||||||
|
|
||||||
|
// one file per unique output path
|
||||||
|
if (!destFiles.add(to)) {
|
||||||
|
issues.add("Duplicate destination path");
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if destination path already exists
|
||||||
|
if (to.exists() && !to.equals(from)) {
|
||||||
|
issues.add("File already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (issues.size() > 0) {
|
||||||
|
conflicts.add(new Conflict(from, to, issues));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (conflicts.isEmpty()) {
|
||||||
|
return renameMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConflictDialog dialog = new ConflictDialog(getWindow(parent), conflicts);
|
||||||
|
dialog.setVisible(true);
|
||||||
|
|
||||||
|
if (dialog.cancel()) {
|
||||||
|
return emptyMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// exclude conflicts from rename map
|
||||||
|
for (Conflict it : dialog.getConflicts()) {
|
||||||
|
renameMap.remove(it.source);
|
||||||
|
}
|
||||||
|
return renameMap;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ConflictTableModel model = new ConflictTableModel();
|
||||||
|
private boolean cancel = true;
|
||||||
|
|
||||||
|
public ConflictDialog(Window owner, List<Conflict> conflicts) {
|
||||||
|
super(owner, "Conflicting Files", ModalityType.DOCUMENT_MODAL);
|
||||||
|
|
||||||
|
model.setData(conflicts);
|
||||||
|
|
||||||
|
JTable table = new JTable(model);
|
||||||
|
table.setDefaultRenderer(File.class, new FileRenderer());
|
||||||
|
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(30);
|
||||||
|
table.setRowHeight(20);
|
||||||
|
|
||||||
|
table.addMouseListener(new OpenListener());
|
||||||
|
|
||||||
|
// force white background (e.g. gtk-laf default table background is gray)
|
||||||
|
setBackground(Color.WHITE);
|
||||||
|
|
||||||
|
JComponent c = (JComponent) getContentPane();
|
||||||
|
c.setLayout(new MigLayout("insets dialog, nogrid, fill", "", "[fill][pref!]"));
|
||||||
|
|
||||||
|
c.add(new JScrollPane(table), "grow, wrap");
|
||||||
|
|
||||||
|
c.add(newButton("Ignore", ResourceManager.getIcon("dialog.continue"), this::ignore), "tag ok");
|
||||||
|
c.add(newButton("Cancel", ResourceManager.getIcon("dialog.cancel"), this::cancel), "tag cancel");
|
||||||
|
|
||||||
|
if (conflicts.stream().anyMatch(it -> it.destination.exists())) {
|
||||||
|
c.add(newButton("Override", ResourceManager.getIcon("dialog.continue.invalid"), this::override), "tag left");
|
||||||
|
}
|
||||||
|
|
||||||
|
installAction(c, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), newAction("Cancel", this::cancel));
|
||||||
|
|
||||||
|
setDefaultCloseOperation(DISPOSE_ON_CLOSE);
|
||||||
|
pack();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean cancel() {
|
||||||
|
return cancel;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<Conflict> getConflicts() {
|
||||||
|
return model.getData();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void override(ActionEvent evt) {
|
||||||
|
// delete existing destination files and create new data model
|
||||||
|
List<Conflict> data = model.getData().stream().map(c -> {
|
||||||
|
// safety check
|
||||||
|
if (c.source.equals(c.destination)) {
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.destination.exists()) {
|
||||||
|
try {
|
||||||
|
StandardRenameAction.trash(c.destination);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return new Conflict(c.source, c.destination, singletonList(e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolved => remove conflict
|
||||||
|
return null;
|
||||||
|
}).filter(Objects::nonNull).collect(toList());
|
||||||
|
|
||||||
|
// insert new conflict data
|
||||||
|
model.setData(data);
|
||||||
|
|
||||||
|
// continue if there are no more conflicts
|
||||||
|
if (data.isEmpty()) {
|
||||||
|
ignore(evt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ignore(ActionEvent evt) {
|
||||||
|
cancel = false;
|
||||||
|
setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void cancel(ActionEvent evt) {
|
||||||
|
cancel = true;
|
||||||
|
setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Conflict {
|
||||||
|
|
||||||
|
public final File source;
|
||||||
|
public final File destination;
|
||||||
|
|
||||||
|
public final List<Object> issues;
|
||||||
|
|
||||||
|
public Conflict(File source, File destination, List<Object> issues) {
|
||||||
|
this.source = source;
|
||||||
|
this.destination = destination;
|
||||||
|
this.issues = issues;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class ConflictTableModel extends AbstractTableModel {
|
||||||
|
|
||||||
|
private Conflict[] data;
|
||||||
|
|
||||||
|
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";
|
||||||
|
case 4:
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getColumnCount() {
|
||||||
|
return 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getRowCount() {
|
||||||
|
return data.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<?> getColumnClass(int column) {
|
||||||
|
switch (column) {
|
||||||
|
case 0:
|
||||||
|
return Icon.class;
|
||||||
|
case 1:
|
||||||
|
return String.class;
|
||||||
|
default:
|
||||||
|
return File.class;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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.error");
|
||||||
|
case 1:
|
||||||
|
return conflict.issues.isEmpty() ? "OK" : conflict.issues.stream().map(Objects::toString).collect(joining("; "));
|
||||||
|
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) {
|
||||||
|
File f = (File) value;
|
||||||
|
|
||||||
|
super.getTableCellRendererComponent(table, f.getName(), isSelected, hasFocus, row, column);
|
||||||
|
setToolTipText(f.getPath());
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -255,7 +255,7 @@ class RenameAction extends AbstractAction {
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<Entry<File, File>> validate(Map<File, String> renameMap, Window parent) {
|
private List<Entry<File, File>> validate(Map<File, String> renameMap, Window parent) {
|
||||||
final List<Entry<File, File>> source = new ArrayList<Entry<File, File>>(renameMap.size());
|
List<Entry<File, File>> source = new ArrayList<Entry<File, File>>(renameMap.size());
|
||||||
|
|
||||||
for (Entry<File, String> entry : renameMap.entrySet()) {
|
for (Entry<File, String> entry : renameMap.entrySet()) {
|
||||||
source.add(new SimpleEntry<File, File>(entry.getKey(), new File(entry.getValue())));
|
source.add(new SimpleEntry<File, File>(entry.getKey(), new File(entry.getValue())));
|
||||||
|
|
|
@ -105,7 +105,7 @@ public class RenameModel extends MatchModel<Object, File> {
|
||||||
|
|
||||||
// insert mapping
|
// insert mapping
|
||||||
if (map.put(source, destination.toString()) != null) {
|
if (map.put(source, destination.toString()) != null) {
|
||||||
throw new IllegalStateException(String.format("Duplicate file: \"%s\"", source.getName()));
|
throw new IllegalStateException("Duplicate source file: " + source.getName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue