mirror of
https://github.com/mitb-archive/filebot
synced 2024-08-13 17:03:45 -04:00
978 lines
35 KiB
Java
978 lines
35 KiB
Java
package net.filebot.ui.rename;
|
|
|
|
import static java.awt.event.KeyEvent.*;
|
|
import static java.util.Collections.*;
|
|
import static java.util.Comparator.*;
|
|
import static javax.swing.KeyStroke.*;
|
|
import static javax.swing.SwingUtilities.*;
|
|
import static net.filebot.Logging.*;
|
|
import static net.filebot.Settings.*;
|
|
import static net.filebot.media.MediaDetection.*;
|
|
import static net.filebot.util.ExceptionUtilities.*;
|
|
import static net.filebot.util.FileUtilities.*;
|
|
import static net.filebot.util.ui.LoadingOverlayPane.*;
|
|
import static net.filebot.util.ui.SwingUI.*;
|
|
|
|
import java.awt.Component;
|
|
import java.awt.Cursor;
|
|
import java.awt.Dimension;
|
|
import java.awt.Insets;
|
|
import java.awt.Window;
|
|
import java.awt.datatransfer.Transferable;
|
|
import java.awt.event.ActionEvent;
|
|
import java.io.File;
|
|
import java.util.ArrayList;
|
|
import java.util.LinkedList;
|
|
import java.util.List;
|
|
import java.util.Locale;
|
|
import java.util.Map;
|
|
import java.util.Optional;
|
|
import java.util.concurrent.CancellationException;
|
|
import java.util.function.Supplier;
|
|
import java.util.logging.Level;
|
|
|
|
import javax.swing.AbstractAction;
|
|
import javax.swing.Action;
|
|
import javax.swing.DefaultListCellRenderer;
|
|
import javax.swing.Icon;
|
|
import javax.swing.JButton;
|
|
import javax.swing.JComboBox;
|
|
import javax.swing.JComponent;
|
|
import javax.swing.JList;
|
|
import javax.swing.JOptionPane;
|
|
import javax.swing.JPanel;
|
|
import javax.swing.JScrollPane;
|
|
import javax.swing.SwingConstants;
|
|
import javax.swing.SwingUtilities;
|
|
import javax.swing.SwingWorker;
|
|
import javax.swing.border.CompoundBorder;
|
|
import javax.swing.border.TitledBorder;
|
|
|
|
import com.google.common.eventbus.Subscribe;
|
|
|
|
import ca.odell.glazedlists.EventList;
|
|
import ca.odell.glazedlists.ListSelection;
|
|
import ca.odell.glazedlists.swing.DefaultEventSelectionModel;
|
|
import net.filebot.ApplicationFolder;
|
|
import net.filebot.CacheManager;
|
|
import net.filebot.History;
|
|
import net.filebot.HistorySpooler;
|
|
import net.filebot.InvalidResponseException;
|
|
import net.filebot.Language;
|
|
import net.filebot.ResourceManager;
|
|
import net.filebot.Settings;
|
|
import net.filebot.StandardRenameAction;
|
|
import net.filebot.UserFiles;
|
|
import net.filebot.WebServices;
|
|
import net.filebot.format.ExpressionFileFormat;
|
|
import net.filebot.format.MediaBindingBean;
|
|
import net.filebot.media.LocalDatasource;
|
|
import net.filebot.media.MetaAttributes;
|
|
import net.filebot.platform.mac.MacAppUtilities;
|
|
import net.filebot.similarity.Match;
|
|
import net.filebot.ui.SelectDialog;
|
|
import net.filebot.ui.rename.FormatDialog.Mode;
|
|
import net.filebot.ui.rename.RenameModel.FormattedFuture;
|
|
import net.filebot.ui.transfer.BackgroundFileTransferablePolicy;
|
|
import net.filebot.ui.transfer.TransferablePolicy;
|
|
import net.filebot.ui.transfer.TransferablePolicy.TransferAction;
|
|
import net.filebot.util.AlphanumComparator;
|
|
import net.filebot.util.PreferencesMap.PreferencesEntry;
|
|
import net.filebot.util.ui.ActionPopup;
|
|
import net.filebot.util.ui.DefaultFancyListCellRenderer;
|
|
import net.filebot.util.ui.LoadingOverlayPane;
|
|
import net.filebot.vfs.FileInfo;
|
|
import net.filebot.vfs.SimpleFileInfo;
|
|
import net.filebot.web.AudioTrack;
|
|
import net.filebot.web.AudioTrackFormat;
|
|
import net.filebot.web.Episode;
|
|
import net.filebot.web.EpisodeFormat;
|
|
import net.filebot.web.EpisodeListProvider;
|
|
import net.filebot.web.Movie;
|
|
import net.filebot.web.MovieFormat;
|
|
import net.filebot.web.MovieIdentificationService;
|
|
import net.filebot.web.MusicIdentificationService;
|
|
import net.filebot.web.SortOrder;
|
|
import net.miginfocom.swing.MigLayout;
|
|
|
|
public class RenamePanel extends JComponent {
|
|
|
|
public static final String MATCH_MODE_OPPORTUNISTIC = "Opportunistic";
|
|
public static final String MATCH_MODE_STRICT = "Strict";
|
|
|
|
protected final RenameModel renameModel = new RenameModel();
|
|
|
|
protected final RenameList<FormattedFuture> namesList = new RenameList<FormattedFuture>(renameModel.names());
|
|
|
|
protected final RenameList<File> filesList = new RenameList<File>(renameModel.files());
|
|
|
|
protected final MatchAction matchAction = new MatchAction(renameModel);
|
|
|
|
protected final RenameAction renameAction = new RenameAction(renameModel);
|
|
|
|
private static final PreferencesEntry<String> persistentEpisodeFormat = Settings.forPackage(RenamePanel.class).entry("rename.format.episode");
|
|
private static final PreferencesEntry<String> persistentMovieFormat = Settings.forPackage(RenamePanel.class).entry("rename.format.movie");
|
|
private static final PreferencesEntry<String> persistentMusicFormat = Settings.forPackage(RenamePanel.class).entry("rename.format.music");
|
|
private static final PreferencesEntry<String> persistentFileFormat = Settings.forPackage(RenamePanel.class).entry("rename.format.file");
|
|
|
|
private static final PreferencesEntry<String> persistentLastFormatState = Settings.forPackage(RenamePanel.class).entry("rename.last.format.state").defaultValue(Mode.Episode.name());
|
|
private static final PreferencesEntry<String> persistentPreferredMatchMode = Settings.forPackage(RenamePanel.class).entry("rename.match.mode").defaultValue(MATCH_MODE_OPPORTUNISTIC);
|
|
private static final PreferencesEntry<String> persistentPreferredLanguage = Settings.forPackage(RenamePanel.class).entry("rename.language").defaultValue("en");
|
|
private static final PreferencesEntry<String> persistentPreferredEpisodeOrder = Settings.forPackage(RenamePanel.class).entry("rename.episode.order").defaultValue("Airdate");
|
|
|
|
private static final Map<String, Preset> persistentPresets = Settings.forPackage(RenamePanel.class).node("presets").asMap(Preset.class);
|
|
|
|
public RenamePanel() {
|
|
namesList.setTitle("New Names");
|
|
namesList.setTransferablePolicy(new NamesListTransferablePolicy(renameModel.values()));
|
|
|
|
filesList.setTitle("Original Files");
|
|
filesList.setTransferablePolicy(new FilesListTransferablePolicy(renameModel.files()));
|
|
|
|
// restore icon indicating current match mode
|
|
matchAction.setMatchMode(isMatchModeStrict());
|
|
|
|
try {
|
|
// restore custom episode formatter
|
|
renameModel.useFormatter(Episode.class, new ExpressionFormatter(persistentEpisodeFormat.getValue(), EpisodeFormat.SeasonEpisode, Episode.class));
|
|
} catch (Exception e) {
|
|
// use default formatter
|
|
}
|
|
|
|
try {
|
|
// restore custom movie formatter
|
|
renameModel.useFormatter(Movie.class, new ExpressionFormatter(persistentMovieFormat.getValue(), MovieFormat.NameYear, Movie.class));
|
|
} catch (Exception e) {
|
|
// use default movie formatter
|
|
renameModel.useFormatter(Movie.class, new MovieFormatter());
|
|
}
|
|
|
|
try {
|
|
// restore custom music formatter
|
|
renameModel.useFormatter(AudioTrack.class, new ExpressionFormatter(persistentMusicFormat.getValue(), new AudioTrackFormat(), AudioTrack.class));
|
|
} catch (Exception e) {
|
|
// use default formatter
|
|
}
|
|
|
|
try {
|
|
// restore custom music formatter
|
|
renameModel.useFormatter(File.class, new ExpressionFormatter(persistentFileFormat.getValue(), new FileNameFormat(), File.class));
|
|
} catch (Exception e) {
|
|
// make sure to put File formatter at position 3
|
|
renameModel.useFormatter(File.class, new FileNameFormatter());
|
|
} finally {
|
|
// use default filename formatter
|
|
renameModel.useFormatter(FileInfo.class, new FileNameFormatter());
|
|
}
|
|
|
|
RenameListCellRenderer cellrenderer = new RenameListCellRenderer(renameModel, ApplicationFolder.UserHome.get());
|
|
|
|
namesList.getListComponent().setCellRenderer(cellrenderer);
|
|
filesList.getListComponent().setCellRenderer(cellrenderer);
|
|
|
|
DefaultEventSelectionModel<Match<Object, File>> selectionModel = new DefaultEventSelectionModel<Match<Object, File>>(renameModel.matches());
|
|
selectionModel.setSelectionMode(ListSelection.SINGLE_SELECTION);
|
|
|
|
// use the same selection model for both lists to synchronize selection
|
|
namesList.getListComponent().setSelectionModel(selectionModel);
|
|
filesList.getListComponent().setSelectionModel(selectionModel);
|
|
|
|
// synchronize viewports
|
|
new ScrollPaneSynchronizer(namesList, filesList);
|
|
|
|
// delete items from both lists
|
|
Action removeAction = newAction("Exclude Selected Items", ResourceManager.getIcon("dialog.cancel"), evt -> {
|
|
RenameList list = null;
|
|
boolean deleteCell;
|
|
|
|
if (evt.getSource() instanceof JButton) {
|
|
list = filesList;
|
|
deleteCell = isShiftOrAltDown(evt);
|
|
} else {
|
|
list = ((RenameList) evt.getSource());
|
|
deleteCell = isShiftOrAltDown(evt);
|
|
}
|
|
|
|
int index = list.getListComponent().getSelectedIndex();
|
|
if (index >= 0) {
|
|
if (deleteCell) {
|
|
EventList eventList = list.getModel();
|
|
if (index < eventList.size()) {
|
|
list.getModel().remove(index);
|
|
}
|
|
} else {
|
|
renameModel.matches().remove(index);
|
|
}
|
|
int maxIndex = list.getModel().size() - 1;
|
|
if (index > maxIndex) {
|
|
index = maxIndex;
|
|
}
|
|
if (index >= 0) {
|
|
list.getListComponent().setSelectedIndex(index);
|
|
}
|
|
}
|
|
});
|
|
namesList.setRemoveAction(removeAction);
|
|
filesList.setRemoveAction(removeAction);
|
|
|
|
// create Match button
|
|
JButton matchButton = new JButton(matchAction);
|
|
matchButton.setVerticalTextPosition(SwingConstants.BOTTOM);
|
|
matchButton.setHorizontalTextPosition(SwingConstants.CENTER);
|
|
|
|
// create Rename button
|
|
JButton renameButton = new JButton(renameAction);
|
|
renameButton.setVerticalTextPosition(SwingConstants.BOTTOM);
|
|
renameButton.setHorizontalTextPosition(SwingConstants.CENTER);
|
|
|
|
// create fetch popup
|
|
ActionPopup fetchPopup = createFetchPopup();
|
|
|
|
Action fetchPopupAction = new ShowPopupAction("Fetch Data", ResourceManager.getIcon("action.fetch"));
|
|
JButton fetchButton = new JButton(fetchPopupAction);
|
|
filesList.getListComponent().setComponentPopupMenu(fetchPopup);
|
|
namesList.getListComponent().setComponentPopupMenu(fetchPopup);
|
|
fetchButton.setComponentPopupMenu(fetchPopup);
|
|
matchButton.setComponentPopupMenu(fetchPopup);
|
|
namesList.getButtonPanel().add(fetchButton, "gap 0, sgy button");
|
|
|
|
namesList.getListComponent().setComponentPopupMenu(fetchPopup);
|
|
fetchButton.setComponentPopupMenu(fetchPopup);
|
|
|
|
// settings popup and button
|
|
ActionPopup settingsPopup = createSettingsPopup();
|
|
Action settingsPopupAction = new ShowPopupAction("Settings", ResourceManager.getIcon("action.settings"));
|
|
JButton settingsButton = createImageButton(settingsPopupAction);
|
|
settingsButton.setComponentPopupMenu(settingsPopup);
|
|
renameButton.setComponentPopupMenu(settingsPopup);
|
|
namesList.getButtonPanel().add(settingsButton, "gap indent, sgy button");
|
|
|
|
// open rename log button
|
|
filesList.getButtonPanel().add(createImageButton(removeAction), "gap 0, sgy button", 2);
|
|
filesList.getButtonPanel().add(createImageButton(clearFilesAction), "gap 0, sgy button");
|
|
filesList.getButtonPanel().add(createImageButton(openHistoryAction), "gap indent, sgy button");
|
|
|
|
// create macros popup
|
|
JButton presetsButton = createImageButton(new ShowPresetsPopupAction());
|
|
filesList.getButtonPanel().add(presetsButton, "gap 0, sgy button");
|
|
|
|
// show popup on actionPerformed only when names list is empty
|
|
matchButton.addActionListener(evt -> {
|
|
if (renameModel.names().isEmpty()) {
|
|
fetchPopupAction.actionPerformed(evt);
|
|
}
|
|
});
|
|
|
|
// reveal file location on double click
|
|
filesList.getListComponent().addMouseListener(mouseClicked(evt -> {
|
|
if (evt.getClickCount() == 2) {
|
|
getWindow(evt.getSource()).setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR));
|
|
try {
|
|
JList list = (JList) evt.getSource();
|
|
if (list.getSelectedIndex() >= 0) {
|
|
UserFiles.revealFiles(list.getSelectedValuesList());
|
|
}
|
|
} catch (Exception e) {
|
|
debug.log(Level.WARNING, e.getMessage(), e);
|
|
} finally {
|
|
getWindow(evt.getSource()).setCursor(Cursor.getDefaultCursor());
|
|
}
|
|
}
|
|
}));
|
|
|
|
// reveal file location on double click
|
|
namesList.getListComponent().addMouseListener(mouseClicked(evt -> {
|
|
if (evt.getClickCount() == 2) {
|
|
JList list = (JList) evt.getSource();
|
|
if (list.getSelectedIndex() >= 0) {
|
|
Match<Object, File> match = renameModel.getMatch(list.getSelectedIndex());
|
|
Map<File, Object> context = renameModel.getMatchContext(match);
|
|
|
|
if (match.getValue() != null) {
|
|
MediaBindingBean sample = new MediaBindingBean(match.getValue(), match.getCandidate(), context);
|
|
showFormatEditor(sample);
|
|
}
|
|
}
|
|
}
|
|
}));
|
|
|
|
setLayout(new MigLayout("fill, insets dialog, gapx 10px", "[fill][align center, pref!][fill]", "align 33%"));
|
|
add(new LoadingOverlayPane(filesList, filesList, "37px", "30px"), "grow, sizegroupx list");
|
|
|
|
BackgroundFileTransferablePolicy<?> transferablePolicy = (BackgroundFileTransferablePolicy<?>) filesList.getTransferablePolicy();
|
|
transferablePolicy.addPropertyChangeListener(evt -> {
|
|
if (BackgroundFileTransferablePolicy.LOADING_PROPERTY.equals(evt.getPropertyName())) {
|
|
filesList.firePropertyChange(LoadingOverlayPane.LOADING_PROPERTY, (boolean) evt.getOldValue(), (boolean) evt.getNewValue());
|
|
}
|
|
});
|
|
|
|
// make buttons larger
|
|
matchButton.setMargin(new Insets(3, 14, 2, 14));
|
|
renameButton.setMargin(new Insets(6, 11, 2, 11));
|
|
|
|
add(matchButton, "split 2, flowy, sizegroupx button");
|
|
add(renameButton, "gapy 30px, sizegroupx button");
|
|
|
|
add(new LoadingOverlayPane(namesList, namesList, "37px", "30px"), "grow, sizegroupx list");
|
|
|
|
// install F2 and 1..9 keystroke actions
|
|
SwingUtilities.invokeLater(this::installKeyStrokeActions);
|
|
}
|
|
|
|
private void installKeyStrokeActions() {
|
|
// manual force name via F2
|
|
installAction(this, WHEN_IN_FOCUSED_WINDOW, getKeyStroke(VK_F2, 0), newAction("Force Name", evt -> {
|
|
withWaitCursor(evt.getSource(), () -> {
|
|
if (namesList.getModel().isEmpty()) {
|
|
// match to xattr metadata object or the file itself
|
|
Map<File, Object> xattr = LocalDatasource.XATTR.match(renameModel.files(), false);
|
|
|
|
renameModel.clear();
|
|
renameModel.addAll(xattr.values(), xattr.keySet());
|
|
} else {
|
|
int index = namesList.getListComponent().getSelectedIndex();
|
|
if (index >= 0) {
|
|
File file = (File) filesList.getListComponent().getModel().getElementAt(index);
|
|
Object object = namesList.getListComponent().getModel().getElementAt(index);
|
|
|
|
String string = showInputDialog("Enter Name:", object.toString(), "Enter Name", RenamePanel.this);
|
|
if (string != null && string.length() > 0) {
|
|
renameModel.matches().set(index, new Match<Object, File>(string + '.' + getExtension(file), file));
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}));
|
|
|
|
// map 1..9 number keys to presets
|
|
for (int presetNumber = 1; presetNumber <= 9; presetNumber++) {
|
|
int index = presetNumber - 1;
|
|
|
|
installAction(this, WHEN_IN_FOCUSED_WINDOW, getKeyStroke(Character.forDigit(presetNumber, 10), 0), newAction("Preset " + presetNumber, evt -> {
|
|
try {
|
|
List<Preset> presets = getPresets();
|
|
|
|
if (index < presets.size()) {
|
|
new ApplyPresetAction(presets.get(index)).actionPerformed(evt);
|
|
} else {
|
|
new ShowPresetsPopupAction().actionPerformed(evt);
|
|
}
|
|
} catch (Exception e) {
|
|
debug.log(Level.WARNING, e, e::getMessage);
|
|
}
|
|
}));
|
|
}
|
|
|
|
// copy debug information (paths and objects)
|
|
installAction(this, WHEN_IN_FOCUSED_WINDOW, getKeyStroke(VK_F7, 0), newAction("Copy Debug Information", evt -> {
|
|
withWaitCursor(evt.getSource(), () -> {
|
|
String text = getDebugInfo();
|
|
if (text.length() > 0) {
|
|
copyToClipboard(text);
|
|
log.info("Match model has been copied to clipboard");
|
|
} else {
|
|
log.warning("Match model is empty");
|
|
}
|
|
});
|
|
}));
|
|
}
|
|
|
|
private boolean isMatchModeStrict() {
|
|
return MATCH_MODE_STRICT.equalsIgnoreCase(persistentPreferredMatchMode.getValue());
|
|
}
|
|
|
|
private ActionPopup createPresetsPopup() {
|
|
ActionPopup actionPopup = new ActionPopup("Presets", ResourceManager.getIcon("action.script"));
|
|
|
|
List<Preset> presets = getPresets();
|
|
if (presets.size() > 0) {
|
|
for (Preset preset : presets) {
|
|
actionPopup.add(new ApplyPresetAction(preset));
|
|
}
|
|
actionPopup.addSeparator();
|
|
}
|
|
|
|
actionPopup.add(newAction("Edit Presets", ResourceManager.getIcon("script.add"), evt -> {
|
|
Window window = getWindow(evt.getSource());
|
|
|
|
Action newPreset = newAction("New Preset …", ResourceManager.getIcon("script.add"), a -> {
|
|
Optional.ofNullable(JOptionPane.showInputDialog(window, "Preset Name:", a.getActionCommand(), JOptionPane.PLAIN_MESSAGE, null, null, "My Preset")).map(Object::toString).map(String::trim).filter(s -> s.length() > 0).ifPresent(n -> {
|
|
showPresetEditor(new Preset(n, null, null, null, null, null, null, null, null), window);
|
|
});
|
|
});
|
|
|
|
List<Object> options = new ArrayList<Object>(presets);
|
|
options.add(newPreset);
|
|
|
|
SelectDialog<Object> selectDialog = new SelectDialog<Object>(window, options) {
|
|
|
|
@Override
|
|
protected void configureValue(DefaultFancyListCellRenderer render, Object value) {
|
|
if (value instanceof Preset) {
|
|
Preset preset = (Preset) value;
|
|
render.setIcon(preset.getIcon());
|
|
} else if (value instanceof Action) {
|
|
Action action = (Action) value;
|
|
render.setText((String) action.getValue(Action.NAME));
|
|
render.setIcon((Icon) action.getValue(Action.SMALL_ICON));
|
|
}
|
|
}
|
|
};
|
|
|
|
selectDialog.setTitle("Edit Presets");
|
|
selectDialog.setLocation(getOffsetLocation(selectDialog.getOwner()));
|
|
selectDialog.setMinimumSize(new Dimension(250, 250));
|
|
selectDialog.pack();
|
|
selectDialog.setVisible(true);
|
|
|
|
Object selection = selectDialog.getSelectedValue();
|
|
if (selection instanceof Preset) {
|
|
Preset preset = (Preset) selection;
|
|
showPresetEditor(preset, window);
|
|
} else if (selection instanceof Action) {
|
|
Action action = (Action) selection;
|
|
action.actionPerformed(new ActionEvent(newPreset, ActionEvent.ACTION_PERFORMED, "New Preset …"));
|
|
}
|
|
}));
|
|
|
|
return actionPopup;
|
|
}
|
|
|
|
private ActionPopup createFetchPopup() {
|
|
ActionPopup actionPopup = new ActionPopup("Fetch & Match Data", ResourceManager.getIcon("action.fetch"));
|
|
|
|
actionPopup.addDescription("Episode Mode:");
|
|
|
|
// create actions for match popup episode list completion
|
|
for (EpisodeListProvider db : WebServices.getEpisodeListProviders()) {
|
|
actionPopup.add(new AutoCompleteAction(db.getName(), db.getIcon(), () -> new EpisodeListMatcher(db, db == WebServices.AniDB)));
|
|
}
|
|
|
|
actionPopup.addSeparator();
|
|
actionPopup.addDescription("Movie Mode:");
|
|
|
|
// create action for movie name completion
|
|
for (MovieIdentificationService it : WebServices.getMovieIdentificationServices()) {
|
|
actionPopup.add(new AutoCompleteAction(it.getName(), it.getIcon(), () -> new MovieMatcher(it)));
|
|
}
|
|
|
|
actionPopup.addSeparator();
|
|
actionPopup.addDescription("Music Mode:");
|
|
for (MusicIdentificationService it : WebServices.getMusicIdentificationServices()) {
|
|
actionPopup.add(new AutoCompleteAction(it.getName(), it.getIcon(), () -> new MusicMatcher(it)));
|
|
}
|
|
|
|
actionPopup.addSeparator();
|
|
actionPopup.addDescription("Smart Mode:");
|
|
actionPopup.add(new AutoCompleteAction("Autodetect", ResourceManager.getIcon("action.auto"), AutoDetectMatcher::new));
|
|
|
|
actionPopup.addSeparator();
|
|
actionPopup.addDescription("Options:");
|
|
|
|
actionPopup.add(newAction("Edit Format", ResourceManager.getIcon("action.format"), evt -> showFormatEditor(null)));
|
|
|
|
actionPopup.add(newAction("Preferences", ResourceManager.getIcon("action.preferences"), evt -> {
|
|
String[] modes = new String[] { MATCH_MODE_OPPORTUNISTIC, MATCH_MODE_STRICT };
|
|
JComboBox modeCombo = new JComboBox(modes);
|
|
|
|
List<Language> languages = new ArrayList<Language>();
|
|
languages.addAll(Language.preferredLanguages()); // add preferred languages first
|
|
languages.addAll(Language.availableLanguages()); // then others
|
|
|
|
JComboBox orderCombo = new JComboBox(SortOrder.values());
|
|
JList languageList = new JList(languages.toArray());
|
|
languageList.setCellRenderer(new DefaultListCellRenderer() {
|
|
|
|
@Override
|
|
public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
|
|
super.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus);
|
|
if (value != null) {
|
|
setText(((Language) value).getName());
|
|
setIcon(ResourceManager.getFlagIcon(((Language) value).getCode()));
|
|
}
|
|
return this;
|
|
}
|
|
});
|
|
|
|
// restore current preference values
|
|
try {
|
|
modeCombo.setSelectedItem(persistentPreferredMatchMode.getValue());
|
|
orderCombo.setSelectedItem(SortOrder.forName(persistentPreferredEpisodeOrder.getValue()));
|
|
|
|
String selectedLanguage = persistentPreferredLanguage.getValue();
|
|
languages.stream().filter(l -> l.getCode().equals(selectedLanguage)).findFirst().ifPresent(l -> languageList.setSelectedValue(l, true));
|
|
} catch (Exception e) {
|
|
debug.log(Level.WARNING, e.getMessage(), e);
|
|
}
|
|
|
|
JScrollPane spModeCombo = new JScrollPane(modeCombo, JScrollPane.VERTICAL_SCROLLBAR_NEVER, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
|
|
spModeCombo.setBorder(new CompoundBorder(new TitledBorder("Match Mode"), spModeCombo.getBorder()));
|
|
JScrollPane spLanguageList = new JScrollPane(languageList);
|
|
spLanguageList.setBorder(new CompoundBorder(new TitledBorder("Language"), spLanguageList.getBorder()));
|
|
JScrollPane spOrderCombo = new JScrollPane(orderCombo, JScrollPane.VERTICAL_SCROLLBAR_NEVER, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
|
|
spOrderCombo.setBorder(new CompoundBorder(new TitledBorder("Episode Order"), spOrderCombo.getBorder()));
|
|
|
|
// fix background issues on OSX
|
|
spModeCombo.setOpaque(false);
|
|
spLanguageList.setOpaque(false);
|
|
spOrderCombo.setOpaque(false);
|
|
|
|
JPanel message = new JPanel(new MigLayout("fill, flowy, insets 0"));
|
|
message.add(spModeCombo, "grow, hmin 24px");
|
|
message.add(spLanguageList, "grow, hmin 50px");
|
|
message.add(spOrderCombo, "grow, hmin 24px");
|
|
JOptionPane pane = new JOptionPane(message, JOptionPane.PLAIN_MESSAGE, JOptionPane.OK_CANCEL_OPTION);
|
|
pane.createDialog(getWindowAncestor(RenamePanel.this), "Preferences").setVisible(true);
|
|
|
|
if (pane.getValue() != null && pane.getValue().equals(JOptionPane.OK_OPTION)) {
|
|
persistentPreferredMatchMode.setValue((String) modeCombo.getSelectedItem());
|
|
persistentPreferredLanguage.setValue(((Language) languageList.getSelectedValue()).getCode());
|
|
persistentPreferredEpisodeOrder.setValue(((SortOrder) orderCombo.getSelectedItem()).name());
|
|
|
|
// update UI
|
|
matchAction.setMatchMode(isMatchModeStrict());
|
|
}
|
|
}));
|
|
|
|
return actionPopup;
|
|
}
|
|
|
|
private ActionPopup createSettingsPopup() {
|
|
ActionPopup actionPopup = new ActionPopup("Rename Options", ResourceManager.getIcon("action.settings"));
|
|
|
|
actionPopup.addDescription("Extension:");
|
|
actionPopup.add(new SetRenameMode(false, "Preserve", ResourceManager.getIcon("action.extension.preserve")));
|
|
actionPopup.add(new SetRenameMode(true, "Override", ResourceManager.getIcon("action.extension.override")));
|
|
|
|
actionPopup.addSeparator();
|
|
|
|
actionPopup.addDescription("Action:");
|
|
for (StandardRenameAction action : Preset.getSupportedActions()) {
|
|
actionPopup.add(new SetRenameAction(action));
|
|
}
|
|
|
|
return actionPopup;
|
|
}
|
|
|
|
private Mode getFormatEditorMode(MediaBindingBean binding) {
|
|
if (binding != null) {
|
|
if (binding.getInfoObject() instanceof Episode) {
|
|
return Mode.Episode;
|
|
} else if (binding.getInfoObject() instanceof Movie) {
|
|
return Mode.Movie;
|
|
} else if (binding.getInfoObject() instanceof AudioTrack) {
|
|
return Mode.Music;
|
|
} else if (binding.getInfoObject() instanceof File) {
|
|
return Mode.File;
|
|
} else {
|
|
throw new IllegalArgumentException("Cannot format class: " + binding.getInfoObjectType()); // ignore objects that cannot be formatted
|
|
}
|
|
}
|
|
|
|
try {
|
|
return Mode.valueOf(persistentLastFormatState.getValue()); // restore previous mode
|
|
} catch (Exception e) {
|
|
debug.log(Level.WARNING, e, e::getMessage);
|
|
}
|
|
|
|
return Mode.Episode; // default to Episode mode
|
|
}
|
|
|
|
private void showFormatEditor(MediaBindingBean binding) {
|
|
withWaitCursor(this, () -> {
|
|
FormatDialog dialog = new FormatDialog(getWindowAncestor(RenamePanel.this), getFormatEditorMode(binding), binding, binding != null);
|
|
dialog.setLocation(getOffsetLocation(dialog.getOwner()));
|
|
dialog.setVisible(true);
|
|
|
|
if (dialog.submit()) {
|
|
switch (dialog.getMode()) {
|
|
case Episode:
|
|
renameModel.useFormatter(Episode.class, new ExpressionFormatter(dialog.getFormat().getExpression(), EpisodeFormat.SeasonEpisode, Episode.class));
|
|
persistentEpisodeFormat.setValue(dialog.getFormat().getExpression());
|
|
break;
|
|
case Movie:
|
|
renameModel.useFormatter(Movie.class, new ExpressionFormatter(dialog.getFormat().getExpression(), MovieFormat.NameYear, Movie.class));
|
|
persistentMovieFormat.setValue(dialog.getFormat().getExpression());
|
|
break;
|
|
case Music:
|
|
renameModel.useFormatter(AudioTrack.class, new ExpressionFormatter(dialog.getFormat().getExpression(), new AudioTrackFormat(), AudioTrack.class));
|
|
persistentMusicFormat.setValue(dialog.getFormat().getExpression());
|
|
break;
|
|
case File:
|
|
renameModel.useFormatter(File.class, new ExpressionFormatter(dialog.getFormat().getExpression(), new FileNameFormat(), File.class));
|
|
persistentFileFormat.setValue(dialog.getFormat().getExpression());
|
|
break;
|
|
}
|
|
|
|
if (binding == null) {
|
|
persistentLastFormatState.setValue(dialog.getMode().name());
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
private void showPresetEditor(Preset preset, Window owner) {
|
|
try {
|
|
PresetEditor presetEditor = new PresetEditor(owner);
|
|
presetEditor.setPreset(preset);
|
|
|
|
presetEditor.setLocation(getOffsetLocation(presetEditor.getOwner()));
|
|
presetEditor.setVisible(true);
|
|
|
|
switch (presetEditor.getResult()) {
|
|
case SET:
|
|
persistentPresets.put(preset.getName(), presetEditor.getPreset());
|
|
break;
|
|
case DELETE:
|
|
persistentPresets.remove(preset.getName());
|
|
break;
|
|
case CANCEL:
|
|
break;
|
|
}
|
|
} catch (Exception e) {
|
|
debug.log(Level.WARNING, e, e::toString);
|
|
}
|
|
}
|
|
|
|
private List<Preset> getPresets() {
|
|
// load Presets and ensure Preset order on all platforms (e.g. Windows Registry Preferences are sorted alphabetically, but the same is not guaranteed for other platforms)
|
|
List<Preset> presets = new ArrayList<Preset>(persistentPresets.values());
|
|
presets.sort(comparing(Preset::getName, new AlphanumComparator(Locale.getDefault())));
|
|
return presets;
|
|
}
|
|
|
|
private String getDebugInfo() throws Exception {
|
|
StringBuilder sb = new StringBuilder();
|
|
|
|
for (Match<Object, File> m : renameModel.matches()) {
|
|
String f = getStructurePathTail(m.getCandidate()).getPath();
|
|
Object v = m.getValue();
|
|
|
|
// convert FastFile items
|
|
if (v instanceof File) {
|
|
v = new SimpleFileInfo(getStructurePathTail((File) v).getPath(), ((File) v).length());
|
|
}
|
|
|
|
sb.append(f).append('\t').append(MetaAttributes.toJson(v)).append('\n');
|
|
}
|
|
|
|
return sb.toString();
|
|
}
|
|
|
|
private final Action clearFilesAction = newAction("Clear All", ResourceManager.getIcon("action.clear"), evt -> {
|
|
if (isShiftOrAltDown(evt)) {
|
|
renameModel.files().clear();
|
|
} else {
|
|
renameModel.clear();
|
|
}
|
|
});
|
|
|
|
private final Action openHistoryAction = newAction("Open History", ResourceManager.getIcon("action.report"), evt -> {
|
|
try {
|
|
History model = HistorySpooler.getInstance().getCompleteHistory();
|
|
|
|
HistoryDialog dialog = new HistoryDialog(getWindow(RenamePanel.this));
|
|
dialog.setLocationRelativeTo(RenamePanel.this);
|
|
dialog.setModel(model);
|
|
|
|
// show and block
|
|
dialog.setVisible(true);
|
|
} catch (Exception e) {
|
|
log.log(Level.WARNING, e, cause(getRootCause(e)));
|
|
}
|
|
});
|
|
|
|
@Subscribe
|
|
public void handle(Transferable transferable) throws Exception {
|
|
for (TransferablePolicy handler : new TransferablePolicy[] { filesList.getTransferablePolicy(), namesList.getTransferablePolicy() }) {
|
|
if (handler != null && handler.accept(transferable)) {
|
|
handler.handleTransferable(transferable, TransferAction.PUT);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static class ShowPopupAction extends AbstractAction {
|
|
|
|
public ShowPopupAction(String name, Icon icon) {
|
|
super(name, icon);
|
|
}
|
|
|
|
@Override
|
|
public void actionPerformed(ActionEvent e) {
|
|
JComponent source = (JComponent) e.getSource();
|
|
source.getComponentPopupMenu().show(source, -3, source.getHeight() + 4);
|
|
}
|
|
};
|
|
|
|
private class ShowPresetsPopupAction extends AbstractAction {
|
|
|
|
public ShowPresetsPopupAction() {
|
|
super("Presets", ResourceManager.getIcon("action.script"));
|
|
}
|
|
|
|
@Override
|
|
public void actionPerformed(ActionEvent evt) {
|
|
try {
|
|
JComponent source = (JComponent) evt.getSource();
|
|
createPresetsPopup().show(source, -3, source.getHeight() + 4);
|
|
} catch (Exception e) {
|
|
debug.log(Level.WARNING, e, e::toString);
|
|
}
|
|
}
|
|
};
|
|
|
|
private class ApplyPresetAction extends AutoCompleteAction {
|
|
|
|
private Preset preset;
|
|
|
|
public ApplyPresetAction(Preset preset) {
|
|
super(preset.getName(), preset.getIcon(), preset::getAutoCompleteMatcher);
|
|
this.preset = preset;
|
|
}
|
|
|
|
@Override
|
|
public List<File> getFiles(ActionEvent evt) {
|
|
File inputFolder = preset.getInputFolder();
|
|
|
|
if (inputFolder == null) {
|
|
return super.getFiles(evt); // default behaviour
|
|
}
|
|
|
|
if (isMacSandbox()) {
|
|
if (!MacAppUtilities.askUnlockFolders(getWindow(RenamePanel.this), singleton(inputFolder))) {
|
|
return emptyList();
|
|
}
|
|
}
|
|
|
|
try {
|
|
List<File> selection = onSecondaryLoop(preset::selectFiles); // run potentially long-running operations on secondary EDT
|
|
|
|
if (selection.size() > 0) {
|
|
renameModel.clear();
|
|
renameModel.files().addAll(selection);
|
|
return selection;
|
|
}
|
|
|
|
log.info("No files have been selected.");
|
|
} catch (Exception e) {
|
|
log.log(Level.WARNING, e, e::toString);
|
|
}
|
|
|
|
return null; // cancel operation
|
|
}
|
|
|
|
@Override
|
|
public boolean isStrict(ActionEvent evt) {
|
|
return preset.getMatchMode() != null ? MATCH_MODE_STRICT.equals(preset.getMatchMode()) : super.isStrict(evt);
|
|
}
|
|
|
|
@Override
|
|
public SortOrder getSortOrder(ActionEvent evt) {
|
|
return preset.getSortOrder() != null ? preset.getSortOrder() : super.getSortOrder(evt);
|
|
}
|
|
|
|
@Override
|
|
public Locale getLocale(ActionEvent evt) {
|
|
return preset.getLanguage() != null ? preset.getLanguage().getLocale() : super.getLocale(evt);
|
|
}
|
|
|
|
@Override
|
|
public void actionPerformed(ActionEvent evt) {
|
|
SwingWorker<ExpressionFormatter, Void> worker = newSwingWorker(() -> {
|
|
ExpressionFileFormat format = preset.getFormat();
|
|
|
|
if (format != null && preset.getDatasource() != null) {
|
|
switch (Mode.getMode(preset.getDatasource())) {
|
|
case Episode:
|
|
return new ExpressionFormatter(format, EpisodeFormat.SeasonEpisode, Episode.class);
|
|
case Movie:
|
|
return new ExpressionFormatter(format, MovieFormat.NameYear, Movie.class);
|
|
case Music:
|
|
return new ExpressionFormatter(format, new AudioTrackFormat(), AudioTrack.class);
|
|
case File:
|
|
return new ExpressionFormatter(format, new FileNameFormat(), File.class);
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}, formatter -> {
|
|
if (formatter != null) {
|
|
renameModel.useFormatter(formatter.getTargetClass(), formatter);
|
|
}
|
|
|
|
if (preset.getRenameAction() != null) {
|
|
new SetRenameAction(preset.getRenameAction()).actionPerformed(evt);
|
|
}
|
|
|
|
super.actionPerformed(evt);
|
|
});
|
|
|
|
// auto-match in progress
|
|
namesList.firePropertyChange(LOADING_PROPERTY, false, true);
|
|
worker.execute();
|
|
}
|
|
}
|
|
|
|
private class SetRenameMode extends AbstractAction {
|
|
|
|
private final boolean activate;
|
|
|
|
private SetRenameMode(boolean activate, String name, Icon icon) {
|
|
super(name, icon);
|
|
this.activate = activate;
|
|
}
|
|
|
|
@Override
|
|
public void actionPerformed(ActionEvent evt) {
|
|
renameModel.setPreserveExtension(!activate);
|
|
|
|
// display changed state
|
|
filesList.repaint();
|
|
}
|
|
}
|
|
|
|
private class SetRenameAction extends AbstractAction {
|
|
|
|
private final StandardRenameAction action;
|
|
|
|
public SetRenameAction(StandardRenameAction action) {
|
|
super(action.getDisplayName(), ResourceManager.getIcon("rename.action." + action.name().toLowerCase()));
|
|
this.action = action;
|
|
}
|
|
|
|
@Override
|
|
public void actionPerformed(ActionEvent evt) {
|
|
if (action == StandardRenameAction.MOVE) {
|
|
renameAction.resetValues();
|
|
} else {
|
|
renameAction.putValue(RenameAction.RENAME_ACTION, action);
|
|
renameAction.putValue(NAME, this.getValue(NAME));
|
|
renameAction.putValue(SMALL_ICON, ResourceManager.getIcon("action." + action.name().toLowerCase()));
|
|
}
|
|
}
|
|
}
|
|
|
|
private class AutoCompleteAction extends AbstractAction {
|
|
|
|
protected final Supplier<AutoCompleteMatcher> matcher;
|
|
|
|
public AutoCompleteAction(String name, Icon icon, Supplier<AutoCompleteMatcher> matcher) {
|
|
super(name, icon);
|
|
|
|
// create matcher when required
|
|
this.matcher = matcher;
|
|
|
|
// disable action while episode list matcher is working
|
|
namesList.addPropertyChangeListener(LOADING_PROPERTY, evt -> {
|
|
// disable action while loading is in progress
|
|
setEnabled(!(Boolean) evt.getNewValue());
|
|
});
|
|
}
|
|
|
|
public List<File> getFiles(ActionEvent evt) {
|
|
return renameModel.files();
|
|
}
|
|
|
|
public boolean isStrict(ActionEvent evt) {
|
|
return isMatchModeStrict();
|
|
}
|
|
|
|
public SortOrder getSortOrder(ActionEvent evt) {
|
|
return SortOrder.forName(persistentPreferredEpisodeOrder.getValue());
|
|
}
|
|
|
|
public Locale getLocale(ActionEvent evt) {
|
|
return Language.getLanguage(persistentPreferredLanguage.getValue()).getLocale();
|
|
}
|
|
|
|
private boolean isAutoDetectionEnabled(ActionEvent evt) {
|
|
return !isShiftOrAltDown(evt); // skip name auto-detection if SHIFT is pressed
|
|
}
|
|
|
|
@Override
|
|
public void actionPerformed(ActionEvent evt) {
|
|
// clear names list
|
|
renameModel.values().clear();
|
|
|
|
// select files
|
|
List<File> files = getFiles(evt);
|
|
if (files == null) {
|
|
namesList.firePropertyChange(LOADING_PROPERTY, true, false);
|
|
return;
|
|
}
|
|
|
|
List<File> remainingFiles = new LinkedList<File>(files);
|
|
boolean strict = isStrict(evt);
|
|
SortOrder order = getSortOrder(evt);
|
|
Locale locale = getLocale(evt);
|
|
boolean autodetection = isAutoDetectionEnabled(evt);
|
|
|
|
if (isMacSandbox()) {
|
|
if (!MacAppUtilities.askUnlockFolders(getWindow(RenamePanel.this), remainingFiles)) {
|
|
namesList.firePropertyChange(LOADING_PROPERTY, true, false);
|
|
return;
|
|
}
|
|
}
|
|
|
|
SwingWorker<List<Match<File, ?>>, Void> worker = new SwingWorker<List<Match<File, ?>>, Void>() {
|
|
|
|
@Override
|
|
protected List<Match<File, ?>> doInBackground() throws Exception {
|
|
List<Match<File, ?>> matches = matcher.get().match(remainingFiles, strict, order, locale, autodetection, getWindow(RenamePanel.this));
|
|
|
|
// flush all memory caches to disk (before starting any long running file system operations that might be cancelled by the user)
|
|
CacheManager.getInstance().flushAll();
|
|
|
|
// remove matched files
|
|
for (Match<File, ?> match : matches) {
|
|
remainingFiles.remove(match.getValue());
|
|
}
|
|
|
|
return matches;
|
|
}
|
|
|
|
@Override
|
|
protected void done() {
|
|
try {
|
|
List<Match<Object, File>> matches = new ArrayList<Match<Object, File>>();
|
|
|
|
for (Match<File, ?> match : get()) {
|
|
matches.add(new Match<Object, File>(match.getCandidate(), match.getValue()));
|
|
}
|
|
|
|
renameModel.clear();
|
|
renameModel.addAll(matches);
|
|
|
|
// add remaining file entries
|
|
renameModel.files().addAll(remainingFiles);
|
|
} catch (Exception e) {
|
|
// ignore cancellation exception
|
|
if (findCause(e, CancellationException.class) != null) {
|
|
return;
|
|
}
|
|
|
|
// common error message
|
|
if (findCause(e, InvalidResponseException.class) != null) {
|
|
log.log(Level.WARNING, findCause(e, InvalidResponseException.class).getMessage());
|
|
return;
|
|
}
|
|
|
|
// generic error message
|
|
log.log(Level.WARNING, e, cause(getRootCause(e)));
|
|
} finally {
|
|
// auto-match finished
|
|
namesList.firePropertyChange(LOADING_PROPERTY, true, false);
|
|
}
|
|
}
|
|
};
|
|
|
|
// auto-match in progress
|
|
namesList.firePropertyChange(LOADING_PROPERTY, false, true);
|
|
worker.execute();
|
|
}
|
|
|
|
}
|
|
|
|
}
|