diff --git a/source/net/filebot/WebServices.java b/source/net/filebot/WebServices.java index 43b40091..7b67c0a8 100644 --- a/source/net/filebot/WebServices.java +++ b/source/net/filebot/WebServices.java @@ -102,7 +102,7 @@ public final class WebServices { return getService(name, getMusicIdentificationServices()); } - private static T getService(String name, T[] services) { + public static T getService(String name, T[] services) { return StreamEx.of(services).findFirst(it -> it.getIdentifier().equalsIgnoreCase(name) || it.getName().equalsIgnoreCase(name)).orElse(null); } diff --git a/source/net/filebot/media/XattrMetaInfoProvider.java b/source/net/filebot/media/XattrMetaInfoProvider.java index 3582fa7e..6d4626b9 100644 --- a/source/net/filebot/media/XattrMetaInfoProvider.java +++ b/source/net/filebot/media/XattrMetaInfoProvider.java @@ -7,6 +7,7 @@ import java.util.Map; import javax.swing.Icon; +import net.filebot.ResourceManager; import net.filebot.web.Datasource; public class XattrMetaInfoProvider implements Datasource { @@ -18,7 +19,7 @@ public class XattrMetaInfoProvider implements Datasource { @Override public Icon getIcon() { - return null; + return ResourceManager.getIcon("search.xattr"); } public Map match(Collection files, boolean strict) { diff --git a/source/net/filebot/resources/search.xattr.png b/source/net/filebot/resources/search.xattr.png new file mode 100644 index 00000000..1619e3de Binary files /dev/null and b/source/net/filebot/resources/search.xattr.png differ diff --git a/source/net/filebot/resources/search.xattr@2x.png b/source/net/filebot/resources/search.xattr@2x.png new file mode 100644 index 00000000..39af4be9 Binary files /dev/null and b/source/net/filebot/resources/search.xattr@2x.png differ diff --git a/source/net/filebot/ui/MainFrame.java b/source/net/filebot/ui/MainFrame.java index b26aef31..2d730c75 100644 --- a/source/net/filebot/ui/MainFrame.java +++ b/source/net/filebot/ui/MainFrame.java @@ -100,33 +100,29 @@ public class MainFrame extends JFrame { })); installAction(getRootPane(), getKeyStroke(VK_F5, 0), newAction("Run", evt -> { - try { - withWaitCursor(getRootPane(), () -> { - GroovyPad pad = new GroovyPad(); + withWaitCursor(getRootPane(), () -> { + GroovyPad pad = new GroovyPad(); - pad.addWindowListener(new WindowAdapter() { - @Override - public void windowOpened(WindowEvent e) { - setVisible(false); + pad.addWindowListener(new WindowAdapter() { + @Override + public void windowOpened(WindowEvent e) { + setVisible(false); - // run default script on startup - pad.runScript(GroovyPad.DEFAULT_SCRIPT); - }; + // run default script on startup + pad.runScript(GroovyPad.DEFAULT_SCRIPT); + }; - @Override - public void windowClosing(WindowEvent e) { - setVisible(true); - }; - }); - - pad.setDefaultCloseOperation(DISPOSE_ON_CLOSE); - pad.setModalExclusionType(ModalExclusionType.TOOLKIT_EXCLUDE); - pad.setLocationByPlatform(true); - pad.setVisible(true); + @Override + public void windowClosing(WindowEvent e) { + setVisible(true); + }; }); - } catch (Exception e) { - debug.log(Level.WARNING, e.getMessage(), e); - } + + pad.setDefaultCloseOperation(DISPOSE_ON_CLOSE); + pad.setModalExclusionType(ModalExclusionType.TOOLKIT_EXCLUDE); + pad.setLocationByPlatform(true); + pad.setVisible(true); + }); })); installAction(this.getRootPane(), getKeyStroke(VK_F1, 0), newAction("Help", evt -> GettingStartedStage.start())); diff --git a/source/net/filebot/ui/rename/ExpressionFormatter.java b/source/net/filebot/ui/rename/ExpressionFormatter.java index b69a781c..db3ab0dd 100644 --- a/source/net/filebot/ui/rename/ExpressionFormatter.java +++ b/source/net/filebot/ui/rename/ExpressionFormatter.java @@ -25,14 +25,26 @@ class ExpressionFormatter implements MatchFormatter { private Class target; public ExpressionFormatter(String expression, Format preview, Class target) { - if (expression == null || expression.isEmpty()) + if (expression == null || expression.isEmpty()) { throw new IllegalArgumentException("Expression must not be null or empty"); + } this.expression = expression; this.preview = preview; this.target = target; } + public ExpressionFormatter(ExpressionFormat format, Format preview, Class target) { + this(format.getExpression(), preview, target); + + // use compiled format expression right away + this.format = format; + } + + public Class getTargetClass() { + return target; + } + @Override public boolean canFormat(Match match) { // target object is required, file is optional diff --git a/source/net/filebot/ui/rename/FormatDialog.java b/source/net/filebot/ui/rename/FormatDialog.java index 25bcfc4c..0f99b087 100644 --- a/source/net/filebot/ui/rename/FormatDialog.java +++ b/source/net/filebot/ui/rename/FormatDialog.java @@ -162,12 +162,13 @@ public class FormatDialog extends JDialog { } public static Mode getMode(Datasource datasource) { - if (datasource instanceof EpisodeListProvider) - return Mode.Episode; if (datasource instanceof MovieIdentificationService) return Mode.Movie; + if (datasource instanceof EpisodeListProvider) + return Mode.Episode; if (datasource instanceof MusicIdentificationService) return Mode.Music; + return Mode.File; } } @@ -376,8 +377,6 @@ public class FormatDialog extends JDialog { return new ExpressionFormat(format).format(sample); }, s -> { formatExample.setText(s); - }, e -> { - debug.log(Level.SEVERE, e.getMessage(), e); }).execute(); }); diff --git a/source/net/filebot/ui/rename/MatchAction.java b/source/net/filebot/ui/rename/MatchAction.java index 63b4f6b0..60fa8f8e 100644 --- a/source/net/filebot/ui/rename/MatchAction.java +++ b/source/net/filebot/ui/rename/MatchAction.java @@ -39,8 +39,8 @@ class MatchAction extends AbstractAction { return; } - try { - withWaitCursor(evt.getSource(), () -> { + withWaitCursor(evt.getSource(), () -> { + try { Matcher matcher = new Matcher(model.values(), model.candidates(), false, EpisodeMetrics.defaultSequence(true)); List> matches = ProgressMonitor.runTask("Match", "Finding optimal alignment. This may take a while.", (message, progress, cancelled) -> { message.accept(String.format("Checking %d combinations...", matcher.remainingCandidates().size() * matcher.remainingValues().size())); @@ -53,12 +53,12 @@ class MatchAction extends AbstractAction { // insert objects that could not be matched at the end of the model model.addAll(matcher.remainingValues(), matcher.remainingCandidates()); - }); - } catch (CancellationException e) { - debug.finest(e::toString); - } catch (Throwable e) { - log.log(Level.WARNING, e.getMessage(), e); - } + } catch (CancellationException e) { + debug.finest(e::toString); + } catch (Throwable e) { + log.log(Level.WARNING, e.getMessage(), e); + } + }); } } diff --git a/source/net/filebot/ui/rename/PlainFileMatcher.java b/source/net/filebot/ui/rename/PlainFileMatcher.java index a0ff4507..68ec530e 100644 --- a/source/net/filebot/ui/rename/PlainFileMatcher.java +++ b/source/net/filebot/ui/rename/PlainFileMatcher.java @@ -17,13 +17,14 @@ import net.filebot.web.SortOrder; public class PlainFileMatcher implements Datasource, AutoCompleteMatcher { - public static PlainFileMatcher getInstance() { - return new PlainFileMatcher(); + @Override + public String getIdentifier() { + return "file"; } @Override public String getName() { - return "Generic File"; + return "Plain File"; } @Override diff --git a/source/net/filebot/ui/rename/Preset.java b/source/net/filebot/ui/rename/Preset.java index eb883363..a72b54bb 100644 --- a/source/net/filebot/ui/rename/Preset.java +++ b/source/net/filebot/ui/rename/Preset.java @@ -1,21 +1,23 @@ package net.filebot.ui.rename; import static java.util.Collections.*; +import static net.filebot.Logging.*; +import static net.filebot.WebServices.*; import static net.filebot.util.FileUtilities.*; -import static net.filebot.util.ui.SwingUI.*; -import java.awt.event.ActionEvent; import java.io.File; +import java.io.FileFilter; import java.util.List; +import java.util.logging.Level; +import java.util.stream.Stream; +import net.filebot.CachedResource.Transform; import net.filebot.Language; -import net.filebot.Settings; import net.filebot.StandardRenameAction; -import net.filebot.WebServices; import net.filebot.format.ExpressionFileFilter; import net.filebot.format.ExpressionFilter; import net.filebot.format.ExpressionFormat; -import net.filebot.mac.MacAppUtilities; +import net.filebot.media.XattrMetaInfoProvider; import net.filebot.web.Datasource; import net.filebot.web.EpisodeListProvider; import net.filebot.web.MovieIdentificationService; @@ -51,114 +53,77 @@ public class Preset { } public File getInputFolder() { - return path == null || path.isEmpty() ? null : new File(path); + return getValue(path, File::new); } public ExpressionFileFilter getIncludeFilter() { - try { - return path == null || path.isEmpty() || includes == null || includes.isEmpty() ? null : new ExpressionFileFilter(new ExpressionFilter(includes), false); - } catch (Exception e) { - return null; - } + return getInputFolder() == null ? null : getValue(includes, expression -> new ExpressionFileFilter(new ExpressionFilter(expression), false)); } public ExpressionFormat getFormat() { - try { - return format == null || format.isEmpty() ? null : new ExpressionFormat(format); - } catch (Exception e) { - return null; - } - } - - public List selectInputFiles(ActionEvent evt) { - File folder = getInputFolder(); - ExpressionFileFilter filter = getIncludeFilter(); - - if (folder == null) { - return null; - } - - if (Settings.isMacSandbox()) { - if (!MacAppUtilities.askUnlockFolders(getWindow(evt.getSource()), singleton(getInputFolder()))) { - throw new IllegalStateException("Unable to access folder: " + folder); - } - } - - return listFiles(getInputFolder(), filter == null ? FILES : filter, HUMAN_NAME_ORDER); - } - - public AutoCompleteMatcher getAutoCompleteMatcher() { - MovieIdentificationService mdb = WebServices.getMovieIdentificationService(database); - if (mdb != null) { - return new MovieMatcher(mdb); - } - - EpisodeListProvider sdb = WebServices.getEpisodeListProvider(database); - if (sdb != null) { - return new EpisodeListMatcher(sdb, sdb == WebServices.AniDB); - } - - MusicIdentificationService adb = WebServices.getMusicIdentificationService(database); - if (adb != null) { - return new MusicMatcher(adb); - } - - if (PlainFileMatcher.getInstance().getIdentifier().equals(database)) { - return PlainFileMatcher.getInstance(); - } - - throw new IllegalStateException(database); - } - - public Datasource getDatasource() { - if (database == null || database.isEmpty()) { - return null; - } - - MovieIdentificationService mdb = WebServices.getMovieIdentificationService(database); - if (mdb != null) { - return mdb; - } - - EpisodeListProvider sdb = WebServices.getEpisodeListProvider(database); - if (sdb != null) { - return sdb; - } - - MusicIdentificationService adb = WebServices.getMusicIdentificationService(database); - if (adb != null) { - return adb; - } - - if (PlainFileMatcher.getInstance().getIdentifier().equals(database)) { - return PlainFileMatcher.getInstance(); - } - - throw new IllegalStateException(database); + return getValue(format, ExpressionFormat::new); } public String getMatchMode() { - return matchMode == null || matchMode.isEmpty() ? null : matchMode; + return getValue(matchMode, mode -> mode); } public SortOrder getSortOrder() { - try { - return SortOrder.forName(sortOrder); - } catch (Exception e) { - return null; - } + return getValue(sortOrder, SortOrder::forName); } public Language getLanguage() { - return language == null || language.isEmpty() ? null : Language.getLanguage(language); + return getValue(language, Language::getLanguage); } public StandardRenameAction getRenameAction() { + return getValue(action, StandardRenameAction::forName); + } + + public Datasource getDatasource() { + return getValue(database, id -> getService(id, getSupportedServices())); + } + + private T getValue(String s, Transform t) { try { - return StandardRenameAction.forName(action); + return s == null || s.isEmpty() ? null : t.transform(s); } catch (Exception e) { - return null; + debug.log(Level.WARNING, e, e::toString); } + return null; + } + + public List selectFiles() { + File folder = getInputFolder(); + if (folder == null || !folder.isDirectory()) { + return emptyList(); + } + + FileFilter filter = getIncludeFilter(); + + return listFiles(folder, filter == null ? FILES : f -> FILES.accept(f) && filter.accept(f), HUMAN_NAME_ORDER); + } + + public AutoCompleteMatcher getAutoCompleteMatcher() { + Datasource db = getDatasource(); + + if (db instanceof MovieIdentificationService) { + return new MovieMatcher((MovieIdentificationService) db); + } + + if (db instanceof EpisodeListProvider) { + return new EpisodeListMatcher((EpisodeListProvider) db, db == AniDB); + } + + if (db instanceof MusicIdentificationService) { + return new MusicMatcher((MusicIdentificationService) db); + } + + if (db instanceof XattrMetaInfoProvider) { + return XATTR_FILE_MATCHER; + } + + return PLAIN_FILE_MATCHER; // default to plain file matcher } @Override @@ -166,4 +131,25 @@ public class Preset { return name; } + public static final XattrFileMatcher XATTR_FILE_MATCHER = new XattrFileMatcher(); + public static final PlainFileMatcher PLAIN_FILE_MATCHER = new PlainFileMatcher(); + + public static Datasource[] getSupportedServices() { + Stream services = Stream.of(getMovieIdentificationServices(), getEpisodeListProviders(), getMusicIdentificationServices()).flatMap(Stream::of); + services = Stream.concat(services, Stream.of(XATTR_FILE_MATCHER, PLAIN_FILE_MATCHER)); + return services.toArray(Datasource[]::new); + } + + public static StandardRenameAction[] getSupportedActions() { + return new StandardRenameAction[] { StandardRenameAction.MOVE, StandardRenameAction.COPY, StandardRenameAction.KEEPLINK, StandardRenameAction.SYMLINK, StandardRenameAction.HARDLINK }; + } + + public static Language[] getSupportedLanguages() { + return Stream.of(Language.preferredLanguages(), Language.availableLanguages()).flatMap(List::stream).toArray(Language[]::new); + } + + public static String[] getSupportedMatchModes() { + return new String[] { RenamePanel.MATCH_MODE_OPPORTUNISTIC, RenamePanel.MATCH_MODE_STRICT }; + } + } diff --git a/source/net/filebot/ui/rename/PresetEditor.java b/source/net/filebot/ui/rename/PresetEditor.java index 851d56c4..1e06cf41 100644 --- a/source/net/filebot/ui/rename/PresetEditor.java +++ b/source/net/filebot/ui/rename/PresetEditor.java @@ -1,30 +1,25 @@ package net.filebot.ui.rename; import static java.awt.Font.*; +import static java.util.Collections.*; import static javax.swing.BorderFactory.*; import static net.filebot.Logging.*; +import static net.filebot.Settings.*; import static net.filebot.util.ui.SwingUI.*; -import java.awt.Component; import java.awt.Font; import java.awt.Window; -import java.awt.event.ActionEvent; import java.io.File; -import java.util.EnumSet; import java.util.List; import java.util.logging.Level; -import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.ButtonGroup; -import javax.swing.DefaultComboBoxModel; import javax.swing.JButton; import javax.swing.JComboBox; import javax.swing.JComponent; import javax.swing.JDialog; import javax.swing.JLabel; -import javax.swing.JList; -import javax.swing.JMenuItem; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JRadioButton; @@ -45,6 +40,7 @@ import net.filebot.WebServices; import net.filebot.format.ExpressionFilter; import net.filebot.format.ExpressionFormat; import net.filebot.format.MediaBindingBean; +import net.filebot.mac.MacAppUtilities; import net.filebot.ui.HeaderPanel; import net.filebot.util.FileUtilities.ExtensionFileFilter; import net.filebot.web.Datasource; @@ -91,7 +87,7 @@ public class PresetEditor extends JDialog { actionCombo = createRenameActionCombo(); providerCombo = createDataProviderCombo(); sortOrderCombo = new JComboBox(SortOrder.values()); - matchModeCombo = createMatchModeCombo(); + matchModeCombo = new JComboBox(Preset.getSupportedMatchModes()); languageCombo = createLanguageCombo(); inputPanel = new JPanel(new MigLayout("insets 0, fill")); @@ -141,7 +137,7 @@ public class PresetEditor extends JDialog { providerCombo.addItemListener((evt) -> updateComponentStates()); updateComponentStates(); - setSize(650, 570); + setSize(730, 570); // add helpful tooltips filterEditor.setToolTipText(FILE_FILTER_TOOLTIP); @@ -237,98 +233,57 @@ public class PresetEditor extends JDialog { } private JComboBox createDataProviderCombo() { - DefaultComboBoxModel providers = new DefaultComboBoxModel<>(); - for (Datasource[] seq : new Datasource[][] { WebServices.getEpisodeListProviders(), WebServices.getMovieIdentificationServices(), WebServices.getMusicIdentificationServices() }) { - for (Datasource it : seq) { - providers.addElement(it); + JComboBox combo = new JComboBox(Preset.getSupportedServices()); + + ListCellRenderer renderer = combo.getRenderer(); + combo.setRenderer((list, value, index, isSelected, cellHasFocus) -> { + JLabel label = (JLabel) renderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + + if (value instanceof Datasource) { + Datasource provider = (Datasource) value; + label.setText(provider.getName()); + label.setIcon(provider.getIcon()); } - } - providers.addElement(PlainFileMatcher.getInstance()); - JComboBox combo = new JComboBox(providers); - combo.setRenderer(new ListCellRenderer() { - - private final ListCellRenderer parent = (ListCellRenderer) combo.getRenderer(); - - @Override - public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) { - JLabel label = (JLabel) parent.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - - if (value instanceof Datasource) { - Datasource provider = (Datasource) value; - label.setText(provider.getName()); - label.setIcon(provider.getIcon()); - } - - return label; - } + return label; }); return combo; } - private JComboBox createMatchModeCombo() { - String[] modes = new String[] { RenamePanel.MATCH_MODE_OPPORTUNISTIC, RenamePanel.MATCH_MODE_STRICT }; - JComboBox combo = new JComboBox<>(modes); - return combo; - } - private JComboBox createLanguageCombo() { - DefaultComboBoxModel languages = new DefaultComboBoxModel<>(); - for (Language it : Language.preferredLanguages()) { - languages.addElement(it); - } - for (Language it : Language.availableLanguages()) { - languages.addElement(it); - } + JComboBox combo = new JComboBox(Preset.getSupportedLanguages()); - JComboBox combo = new JComboBox(languages); - combo.setRenderer(new ListCellRenderer() { + ListCellRenderer renderer = combo.getRenderer(); + combo.setRenderer((list, value, index, isSelected, cellHasFocus) -> { + JLabel label = (JLabel) renderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - private final ListCellRenderer parent = (ListCellRenderer) combo.getRenderer(); - - @Override - public Component getListCellRendererComponent(JList list, Language value, int index, boolean isSelected, boolean cellHasFocus) { - JLabel label = (JLabel) parent.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - - if (value instanceof Language) { - Language it = value; - label.setText(it.getName()); - label.setIcon(ResourceManager.getFlagIcon(it.getCode())); - } - - return label; + if (value instanceof Language) { + Language it = value; + label.setText(it.getName()); + label.setIcon(ResourceManager.getFlagIcon(it.getCode())); } + return label; }); return combo; } private JComboBox createRenameActionCombo() { - DefaultComboBoxModel actions = new DefaultComboBoxModel<>(); - for (StandardRenameAction it : EnumSet.of(StandardRenameAction.MOVE, StandardRenameAction.COPY, StandardRenameAction.KEEPLINK, StandardRenameAction.SYMLINK, StandardRenameAction.HARDLINK)) { - actions.addElement(it); - } + JComboBox combo = new JComboBox(Preset.getSupportedActions()); - JComboBox combo = new JComboBox(actions); - combo.setRenderer(new ListCellRenderer() { + ListCellRenderer renderer = combo.getRenderer(); + combo.setRenderer((list, value, index, isSelected, cellHasFocus) -> { + JLabel label = (JLabel) renderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - private final ListCellRenderer parent = (ListCellRenderer) combo.getRenderer(); - - @Override - public Component getListCellRendererComponent(JList list, RenameAction value, int index, boolean isSelected, boolean cellHasFocus) { - JLabel label = (JLabel) parent.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - - if (value instanceof StandardRenameAction) { - StandardRenameAction it = (StandardRenameAction) value; - label.setText(it.getDisplayName()); - label.setIcon(ResourceManager.getIcon("rename.action." + it.toString().toLowerCase())); - } - - return label; + if (value instanceof StandardRenameAction) { + StandardRenameAction it = (StandardRenameAction) value; + label.setText(it.getDisplayName()); + label.setIcon(ResourceManager.getIcon("rename.action." + it.toString().toLowerCase())); } + return label; }); return combo; @@ -338,104 +293,90 @@ public class PresetEditor extends JDialog { return result; } - private final Action selectInputFolder = new AbstractAction("Select Input Folder", ResourceManager.getIcon("action.load")) { - - @Override - public void actionPerformed(ActionEvent evt) { - File f = UserFiles.showOpenDialogSelectFolder(null, "Select Input Folder", evt); - if (f != null) { - pathInput.setText(f.getAbsolutePath()); - } + private final Action selectInputFolder = newAction("Select Input Folder", ResourceManager.getIcon("action.load"), evt -> { + File f = UserFiles.showOpenDialogSelectFolder(null, "Select Input Folder", evt); + if (f != null) { + pathInput.setText(f.getAbsolutePath()); } - }; + }); - private final Action editFormatExpression = new AbstractAction("Open Format Editor", ResourceManager.getIcon("action.format")) { - - @Override - public void actionPerformed(ActionEvent evt) { - FormatDialog.Mode mode = FormatDialog.Mode.getMode((Datasource) providerCombo.getSelectedItem()); - MediaBindingBean lockOnBinding = null; - if (mode == FormatDialog.Mode.File) { - List files = UserFiles.showLoadDialogSelectFiles(false, false, null, new ExtensionFileFilter(ExtensionFileFilter.WILDCARD), "Select Sample File", evt); - if (files.isEmpty()) { - return; - } - lockOnBinding = new MediaBindingBean(files.get(0), files.get(0)); + private final Action editFormatExpression = newAction("Open Format Editor", ResourceManager.getIcon("action.format"), evt -> { + FormatDialog.Mode mode = FormatDialog.Mode.getMode((Datasource) providerCombo.getSelectedItem()); + MediaBindingBean lockOnBinding = null; + if (mode == FormatDialog.Mode.File) { + List files = UserFiles.showLoadDialogSelectFiles(false, false, null, new ExtensionFileFilter(ExtensionFileFilter.WILDCARD), "Select Sample File", evt); + if (files.isEmpty()) { + return; } - - FormatDialog dialog = new FormatDialog(getWindow(evt.getSource()), mode, lockOnBinding); - dialog.setFormatCode(formatEditor.getText()); - dialog.setLocation(getOffsetLocation(dialog.getOwner())); - dialog.setVisible(true); - - if (dialog.submit()) { - formatEditor.setText(dialog.getFormat().getExpression()); - } - } - }; - - private final Action listFiles = new AbstractAction("List Files", ResourceManager.getIcon("action.search")) { - - private JMenuItem createListItem(ActionEvent evt, File f) { - JMenuItem m = new JMenuItem(f.getPath()); - m.addActionListener((e) -> { - BindingDialog dialog = new BindingDialog(getWindow(evt.getSource()), "File Bindings", FormatDialog.Mode.File.getFormat(), false); - dialog.setLocation(getOffsetLocation(getWindow(evt.getSource()))); - dialog.setInfoObject(f); - dialog.setMediaFile(f); - dialog.setVisible(true); - }); - return m; + lockOnBinding = new MediaBindingBean(files.get(0), files.get(0)); } - @Override - public void actionPerformed(ActionEvent evt) { - try { - withWaitCursor(evt.getSource(), () -> { - List selectInputFiles = getPreset().selectInputFiles(evt); + FormatDialog dialog = new FormatDialog(getWindow(evt.getSource()), mode, lockOnBinding); + dialog.setFormatCode(formatEditor.getText()); + dialog.setLocation(getOffsetLocation(dialog.getOwner())); + dialog.setVisible(true); - JPopupMenu popup = new JPopupMenu(); - if (selectInputFiles == null || selectInputFiles.isEmpty()) { - popup.add("No files selected").setEnabled(false); - } else { - for (File file : selectInputFiles) { - popup.add(createListItem(evt, file)); - } - } - - JComponent source = (JComponent) evt.getSource(); - popup.show(source, -3, source.getHeight() + 4); - }); - } catch (Exception e) { - log.log(Level.WARNING, "Invalid preset settings: " + e.getMessage(), e); - } + if (dialog.submit()) { + formatEditor.setText(dialog.getFormat().getExpression()); } - }; + }); - private final Action ok = new AbstractAction("Save Preset", ResourceManager.getIcon("dialog.continue")) { - - @Override - public void actionPerformed(ActionEvent evt) { + private final Action listFiles = newAction("List Files", ResourceManager.getIcon("action.search"), evt -> { + withWaitCursor(evt.getSource(), () -> { try { Preset preset = getPreset(); - if (preset != null) { - result = Result.SET; - setVisible(false); + if (preset.getInputFolder() == null) { + return; } + + if (isMacSandbox()) { + if (!MacAppUtilities.askUnlockFolders(getWindow(evt.getSource()), singleton(preset.getInputFolder()))) { + return; + } + } + + List files = preset.selectFiles(); + + // display selected files as popup with easy access to more binding info + JPopupMenu popup = new JPopupMenu(); + if (files.size() > 0) { + for (File f : files) { + popup.add(newAction(f.getPath(), e -> { + BindingDialog dialog = new BindingDialog(getWindow(evt.getSource()), "File Bindings", FormatDialog.Mode.File.getFormat(), false); + dialog.setLocation(getOffsetLocation(getWindow(evt.getSource()))); + dialog.setInfoObject(f); + dialog.setMediaFile(f); + dialog.setVisible(true); + })); + } + } else { + popup.add("No files selected").setEnabled(false); + } + + JComponent source = (JComponent) evt.getSource(); + popup.show(source, -3, source.getHeight() + 4); } catch (Exception e) { log.log(Level.WARNING, "Invalid preset settings: " + e.getMessage(), e); } - } - }; + }); + }); - private final Action delete = new AbstractAction("Delete Preset", ResourceManager.getIcon("dialog.cancel")) { - - @Override - public void actionPerformed(ActionEvent evt) { - result = Result.DELETE; - setVisible(false); + private final Action ok = newAction("Save Preset", ResourceManager.getIcon("dialog.continue"), evt -> { + try { + Preset preset = getPreset(); + if (preset != null) { + result = Result.SET; + setVisible(false); + } + } catch (Exception e) { + log.log(Level.WARNING, "Invalid preset settings: " + e.getMessage(), e); } - }; + }); + + private final Action delete = newAction("Delete Preset", ResourceManager.getIcon("dialog.cancel"), evt -> { + result = Result.DELETE; + setVisible(false); + }); private static final String FILE_FILTER_TOOLTIP = "File Selector Expression

e.g.
• fn =~ /alias/
• ext =~ /mp4/
• minutes > 100
• age < 7
• file.isEpisode()
• …
"; diff --git a/source/net/filebot/ui/rename/RenameAction.java b/source/net/filebot/ui/rename/RenameAction.java index 35c9510f..84963aa4 100644 --- a/source/net/filebot/ui/rename/RenameAction.java +++ b/source/net/filebot/ui/rename/RenameAction.java @@ -65,63 +65,59 @@ class RenameAction extends AbstractAction { return; } - try { - Window window = getWindow(evt.getSource()); - withWaitCursor(window, () -> { - Map renameMap = validate(model.getRenameMap(), window); + Window window = getWindow(evt.getSource()); + withWaitCursor(window, () -> { + Map renameMap = validate(model.getRenameMap(), window); - if (renameMap.isEmpty()) { - return; + if (renameMap.isEmpty()) { + return; + } + + List> matches = new ArrayList>(model.matches()); + StandardRenameAction action = (StandardRenameAction) getValue(RENAME_ACTION); + + // start processing + Map renameLog = new LinkedHashMap(); + + try { + if (useNativeShell() && NativeRenameAction.isSupported(action)) { + // call on EDT + NativeRenameWorker worker = new NativeRenameWorker(renameMap, renameLog, NativeRenameAction.valueOf(action.name())); + worker.call(null, null, null); + } else { + // call and wait + StandardRenameWorker worker = new StandardRenameWorker(renameMap, renameLog, action); + String message = String.format("%sing %d %s. This may take a while.", action.getDisplayName(), renameMap.size(), renameMap.size() == 1 ? "file" : "files"); + ProgressMonitor.runTask(action.getDisplayName(), message, worker).get(); } + } catch (CancellationException e) { + debug.finest(e::toString); + } catch (Exception e) { + log.log(Level.SEVERE, String.format("%s: %s", getRootCause(e).getClass().getSimpleName(), getRootCauseMessage(e)), e); + } - List> matches = new ArrayList>(model.matches()); - StandardRenameAction action = (StandardRenameAction) getValue(RENAME_ACTION); + // abort if nothing happened + if (renameLog.isEmpty()) { + return; + } - // start processing - Map renameLog = new LinkedHashMap(); + log.info(String.format("%d files renamed.", renameLog.size())); - try { - if (useNativeShell() && NativeRenameAction.isSupported(action)) { - // call on EDT - NativeRenameWorker worker = new NativeRenameWorker(renameMap, renameLog, NativeRenameAction.valueOf(action.name())); - worker.call(null, null, null); - } else { - // call and wait - StandardRenameWorker worker = new StandardRenameWorker(renameMap, renameLog, action); - String message = String.format("%sing %d %s. This may take a while.", action.getDisplayName(), renameMap.size(), renameMap.size() == 1 ? "file" : "files"); - ProgressMonitor.runTask(action.getDisplayName(), message, worker).get(); - } - } catch (CancellationException e) { - debug.finest(e::toString); - } catch (Exception e) { - log.log(Level.SEVERE, String.format("%s: %s", getRootCause(e).getClass().getSimpleName(), getRootCauseMessage(e)), e); - } - - // abort if nothing happened - if (renameLog.isEmpty()) { - return; - } - - log.info(String.format("%d files renamed.", renameLog.size())); - - // remove renamed matches - renameLog.forEach((from, to) -> { - model.matches().remove(model.files().indexOf(from)); - }); - - HistorySpooler.getInstance().append(renameLog.entrySet()); - - // store xattr - storeMetaInfo(renameMap, matches); - - // delete empty folders - if (action == StandardRenameAction.MOVE) { - deleteEmptyFolders(renameLog); - } + // remove renamed matches + renameLog.forEach((from, to) -> { + model.matches().remove(model.files().indexOf(from)); }); - } catch (Throwable e) { - log.log(Level.WARNING, e.getMessage(), e); - } + + HistorySpooler.getInstance().append(renameLog.entrySet()); + + // store xattr + storeMetaInfo(renameMap, matches); + + // delete empty folders + if (action == StandardRenameAction.MOVE) { + deleteEmptyFolders(renameLog); + } + }); } private void storeMetaInfo(Map renameMap, List> matches) { diff --git a/source/net/filebot/ui/rename/RenamePanel.java b/source/net/filebot/ui/rename/RenamePanel.java index a54bfb03..fb52380c 100644 --- a/source/net/filebot/ui/rename/RenamePanel.java +++ b/source/net/filebot/ui/rename/RenamePanel.java @@ -1,6 +1,7 @@ package net.filebot.ui.rename; import static java.awt.event.KeyEvent.*; +import static java.util.Collections.*; import static javax.swing.JOptionPane.*; import static javax.swing.KeyStroke.*; import static javax.swing.SwingUtilities.*; @@ -15,7 +16,6 @@ import static net.filebot.util.ui.SwingUI.*; import java.awt.Component; import java.awt.Cursor; import java.awt.Insets; -import java.awt.Window; import java.awt.datatransfer.Transferable; import java.awt.event.ActionEvent; import java.io.File; @@ -63,6 +63,7 @@ import net.filebot.Settings; import net.filebot.StandardRenameAction; import net.filebot.UserFiles; import net.filebot.WebServices; +import net.filebot.format.ExpressionFormat; import net.filebot.format.MediaBindingBean; import net.filebot.mac.MacAppUtilities; import net.filebot.media.MetaAttributes; @@ -314,15 +315,13 @@ public class RenamePanel extends JComponent { private void installKeyStrokeActions() { // manual force name via F2 installAction(this, WHEN_IN_FOCUSED_WINDOW, getKeyStroke(VK_F2, 0), newAction("Force Name", evt -> { - try { + withWaitCursor(evt.getSource(), () -> { if (namesList.getModel().isEmpty()) { - withWaitCursor(evt.getSource(), () -> { - // match to xattr metadata object or the file itself - Map xattr = WebServices.XattrMetaData.match(renameModel.files(), false); + // match to xattr metadata object or the file itself + Map xattr = WebServices.XattrMetaData.match(renameModel.files(), false); - renameModel.clear(); - renameModel.addAll(xattr.values(), xattr.keySet()); - }); + renameModel.clear(); + renameModel.addAll(xattr.values(), xattr.keySet()); } else { int index = namesList.getListComponent().getSelectedIndex(); if (index >= 0) { @@ -335,9 +334,7 @@ public class RenamePanel extends JComponent { } } } - } catch (Exception e) { - debug.log(Level.WARNING, e::toString); - } + }); })); // map 1..9 number keys to presets @@ -359,19 +356,15 @@ public class RenamePanel extends JComponent { // copy debug information (paths and objects) installAction(this, WHEN_IN_FOCUSED_WINDOW, getKeyStroke(VK_F7, 0), newAction("Copy Debug Information", evt -> { - try { - 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"); - } - }); - } catch (Exception e) { - debug.log(Level.WARNING, e, e::getMessage); - } + 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"); + } + }); })); } @@ -581,40 +574,36 @@ public class RenamePanel extends JComponent { } private void showFormatEditor(MediaBindingBean binding) { - try { - withWaitCursor(this, () -> { - FormatDialog dialog = new FormatDialog(getWindowAncestor(RenamePanel.this), getFormatEditorMode(binding), binding); - dialog.setLocation(getOffsetLocation(dialog.getOwner())); - dialog.setVisible(true); + withWaitCursor(this, () -> { + FormatDialog dialog = new FormatDialog(getWindowAncestor(RenamePanel.this), getFormatEditorMode(binding), binding); + 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()); - } + 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; } - }); - } catch (Exception e) { - debug.log(Level.WARNING, e::getMessage); - } + + if (binding == null) { + persistentLastFormatState.setValue(dialog.getMode().name()); + } + } + }); } private String getDebugInfo() throws Exception { @@ -705,20 +694,33 @@ public class RenamePanel extends JComponent { @Override public List getFiles(ActionEvent evt) { - List selection = preset.selectInputFiles(evt); + File inputFolder = preset.getInputFolder(); - if (selection != null) { - renameModel.clear(); - renameModel.files().addAll(selection); - } else { - selection = new ArrayList(super.getFiles(evt)); + if (inputFolder == null) { + return super.getFiles(evt); // default behaviour } - if (selection.isEmpty()) { - throw new IllegalStateException("No files selected."); + if (isMacSandbox()) { + if (!MacAppUtilities.askUnlockFolders(getWindow(RenamePanel.this), singleton(inputFolder))) { + return emptyList(); + } } - return selection; + try { + List 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 emptyList(); } @Override @@ -738,40 +740,38 @@ public class RenamePanel extends JComponent { @Override public void actionPerformed(ActionEvent evt) { - Window window = getWindow(RenamePanel.this); - window.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + SwingWorker worker = newSwingWorker(() -> { + ExpressionFormat format = preset.getFormat(); - // Swing Bug Workaround: heavy-weight popup window blocks parent window from being updated (i.e. set wait cursor) unless we wait a little bit until the popup window is destroyed - invokeLater(200, () -> { - try { - if (preset.getFormat() != null) { - switch (FormatDialog.Mode.getMode(preset.getDatasource())) { - case Episode: - renameModel.useFormatter(Episode.class, new ExpressionFormatter(preset.getFormat().getExpression(), EpisodeFormat.SeasonEpisode, Episode.class)); - break; - case Movie: - renameModel.useFormatter(Movie.class, new ExpressionFormatter(preset.getFormat().getExpression(), MovieFormat.NameYear, Movie.class)); - break; - case Music: - renameModel.useFormatter(AudioTrack.class, new ExpressionFormatter(preset.getFormat().getExpression(), new AudioTrackFormat(), AudioTrack.class)); - break; - case File: - renameModel.useFormatter(File.class, new ExpressionFormatter(preset.getFormat().getExpression(), new FileNameFormat(), File.class)); - break; - } + 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); } - - if (preset.getRenameAction() != null) { - new SetRenameAction(preset.getRenameAction()).actionPerformed(evt); - } - - super.actionPerformed(evt); - } catch (Exception e) { - log.log(Level.INFO, e, e::getMessage); - } finally { - window.setCursor(Cursor.getDefaultCursor()); } - }); + + return null; + }, formatter -> { + if (formatter != null) { + renameModel.useFormatter(formatter.getTargetClass(), formatter); + } + + if (preset.getRenameAction() != null) { + new SetRenameAction(preset.getRenameAction()).actionPerformed(evt); + } + + super.actionPerformed(evt); + }, () -> namesList.firePropertyChange(LOADING_PROPERTY, true, false)); + + // auto-match in progress + namesList.firePropertyChange(LOADING_PROPERTY, false, true); + worker.execute(); } } @@ -859,11 +859,15 @@ public class RenamePanel extends JComponent { // clear names list renameModel.values().clear(); - final List remainingFiles = new LinkedList(getFiles(evt)); - final boolean strict = isStrict(evt); - final SortOrder order = getSortOrder(evt); - final Locale locale = getLocale(evt); - final boolean autodetection = isAutoDetectionEnabled(evt); + List remainingFiles = new LinkedList(getFiles(evt)); + if (remainingFiles.isEmpty()) { + return; + } + + 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)) { diff --git a/source/net/filebot/ui/rename/XattrFileMatcher.java b/source/net/filebot/ui/rename/XattrFileMatcher.java new file mode 100644 index 00000000..732c490b --- /dev/null +++ b/source/net/filebot/ui/rename/XattrFileMatcher.java @@ -0,0 +1,38 @@ +package net.filebot.ui.rename; + +import java.awt.Component; +import java.io.File; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Locale; + +import net.filebot.media.XattrMetaInfoProvider; +import net.filebot.similarity.Match; +import net.filebot.web.SortOrder; + +public class XattrFileMatcher extends XattrMetaInfoProvider implements AutoCompleteMatcher { + + @Override + public String getIdentifier() { + return "xattr"; + } + + @Override + public String getName() { + return "Extended Attributes"; + } + + @Override + public List> match(Collection files, boolean strict, SortOrder order, Locale locale, boolean autodetection, Component parent) throws Exception { + List> matches = new ArrayList>(); + + // use strict mode to exclude files that are not xattr tagged + match(files, true).forEach((k, v) -> { + matches.add(new Match(k, v)); + }); + + return matches; + } + +} diff --git a/source/net/filebot/ui/subtitle/upload/MovieEditor.java b/source/net/filebot/ui/subtitle/upload/MovieEditor.java index 2c4c9294..b566a4b6 100644 --- a/source/net/filebot/ui/subtitle/upload/MovieEditor.java +++ b/source/net/filebot/ui/subtitle/upload/MovieEditor.java @@ -99,10 +99,10 @@ class MovieEditor implements TableCellEditor { newSwingWorker(() -> { return runSearch(mapping, table); - }, (options) -> { + }, options -> { runSelect(options, mapping, table); reset(null, table); - }, (error) -> { + }, error -> { reset(error, table); }).execute(); diff --git a/source/net/filebot/util/ui/SwingUI.java b/source/net/filebot/util/ui/SwingUI.java index f179bdf6..427dcee0 100644 --- a/source/net/filebot/util/ui/SwingUI.java +++ b/source/net/filebot/util/ui/SwingUI.java @@ -13,6 +13,7 @@ import java.awt.Frame; import java.awt.Graphics2D; import java.awt.Image; import java.awt.Point; +import java.awt.SecondaryLoop; import java.awt.Toolkit; import java.awt.Window; import java.awt.datatransfer.StringSelection; @@ -27,6 +28,7 @@ import java.lang.reflect.InvocationTargetException; import java.net.URI; import java.util.ArrayList; import java.util.List; +import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.function.Consumer; import java.util.logging.Level; @@ -311,19 +313,17 @@ public final class SwingUI { return timer; } - public static void withWaitCursor(Object source, BackgroundRunnable runnable) throws Exception { - Window window = getWindow(source); - - if (window == null) { - runnable.run(); - return; - } + public static void withWaitCursor(Object source, BackgroundRunnable runnable) { + // window ancestor may be null + Optional window = Optional.ofNullable(getWindow(source)); try { - window.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); + window.ifPresent(w -> w.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR))); runnable.run(); + } catch (Exception e) { + debug.log(Level.SEVERE, e, e::toString); } finally { - window.setCursor(Cursor.getDefaultCursor()); + window.ifPresent(w -> w.setCursor(Cursor.getDefaultCursor())); } } @@ -368,12 +368,37 @@ public final class SwingUI { } } + public static T onSecondaryLoop(BackgroundSupplier supplier) throws ExecutionException, InterruptedException { + // run spawn new EDT and block current EDT + SecondaryLoop secondaryLoop = Toolkit.getDefaultToolkit().getSystemEventQueue().createSecondaryLoop(); + + SwingWorker worker = newSwingWorker(supplier, null, null, () -> secondaryLoop.exit()); + worker.execute(); + + // wait for worker to finish without blocking the EDT + secondaryLoop.enter(); + + return worker.get(); + } + public static SwingWorker newSwingWorker(BackgroundRunnable doInBackground) { return new SwingRunnable(doInBackground); } + public static SwingWorker newSwingWorker(BackgroundSupplier doInBackground, Consumer done) { + return new SwingLambda(doInBackground, done, null, null); + } + public static SwingWorker newSwingWorker(BackgroundSupplier doInBackground, Consumer done, Consumer error) { - return new SwingLambda(doInBackground, done, error); + return new SwingLambda(doInBackground, done, error, null); + } + + public static SwingWorker newSwingWorker(BackgroundSupplier doInBackground, Consumer done, Runnable close) { + return new SwingLambda(doInBackground, done, null, close); + } + + public static SwingWorker newSwingWorker(BackgroundSupplier doInBackground, Consumer done, Consumer error, Runnable close) { + return new SwingLambda(doInBackground, done, error, close); } private static class SwingRunnable extends SwingWorker { @@ -404,13 +429,17 @@ public final class SwingUI { private static class SwingLambda extends SwingWorker { private BackgroundSupplier doInBackground; - private Consumer done; - private Consumer error; + private Optional> done; + private Optional> error; - public SwingLambda(BackgroundSupplier doInBackground, Consumer done, Consumer error) { + private Optional close; + + public SwingLambda(BackgroundSupplier doInBackground, Consumer done, Consumer error, Runnable close) { this.doInBackground = doInBackground; - this.done = done; - this.error = error; + + this.done = Optional.ofNullable(done); + this.error = Optional.ofNullable(error); + this.close = Optional.ofNullable(close); } @Override @@ -421,11 +450,18 @@ public final class SwingUI { @Override protected void done() { try { - done.accept(get()); - } catch (InterruptedException | ExecutionException e) { - error.accept(e); + T value = get(); + done.ifPresent(c -> c.accept(value)); + } catch (Exception e) { + error.orElse(this::printException).accept(e); // print stacktrace by default + } finally { + close.ifPresent(Runnable::run); } } + + private void printException(Exception e) { + debug.log(Level.SEVERE, e, e::toString); + } } /**