package net.filebot.ui; import static javax.swing.ScrollPaneConstants.*; import static net.filebot.Logging.*; import static net.filebot.util.ui.SwingUI.*; import java.awt.Dimension; import java.awt.Window; import java.awt.event.ActionEvent; import java.awt.event.KeyEvent; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.net.URI; import java.util.Arrays; import java.util.Collection; import java.util.TreeSet; import java.util.logging.Level; import javax.swing.AbstractAction; import javax.swing.BorderFactory; import javax.swing.Icon; import javax.swing.JButton; import javax.swing.JComponent; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTabbedPane; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import javax.swing.SwingWorker; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import ca.odell.glazedlists.BasicEventList; import ca.odell.glazedlists.matchers.TextMatcherEditor; import ca.odell.glazedlists.swing.AutoCompleteSupport; import net.filebot.ResourceManager; import net.filebot.Settings; import net.filebot.util.ExceptionUtilities; import net.filebot.util.ui.LabelProvider; import net.filebot.util.ui.SelectButton; import net.filebot.web.SearchResult; import net.miginfocom.swing.MigLayout; public abstract class AbstractSearchPanel<S, E> extends JComponent { protected final JPanel tabbedPaneGroup = new JPanel(new MigLayout("nogrid, fill, insets 0", "align center", "[fill]8px[pref!]4px")); protected final JTabbedPane tabbedPane = new JTabbedPane(); protected final HistoryPanel historyPanel = new HistoryPanel(); protected final SelectButtonTextField<S> searchTextField = new SelectButtonTextField<S>(); protected final BasicEventList<String> searchHistory = new BasicEventList<String>(100000); public AbstractSearchPanel() { historyPanel.setColumnHeader(2, "Duration"); JScrollPane historyScrollPane = new JScrollPane(historyPanel, VERTICAL_SCROLLBAR_AS_NEEDED, HORIZONTAL_SCROLLBAR_NEVER); historyScrollPane.setBorder(BorderFactory.createEmptyBorder()); tabbedPane.addTab("History", ResourceManager.getIcon("action.find"), historyScrollPane); tabbedPaneGroup.setBorder(BorderFactory.createTitledBorder("Search Results")); tabbedPaneGroup.add(tabbedPane, "grow, wrap"); setLayout(new MigLayout("nogrid, novisualpadding, fill, insets 10px 10px 15px 10px", "align 45%", "[pref!]10px[fill]")); add(searchTextField); add(new JButton(searchAction), "gap 16px, h 2+pref!, id search, sgy button"); add(tabbedPaneGroup, "newline, grow"); searchTextField.getEditor().setAction(searchAction); searchTextField.getSelectButton().setModel(Arrays.asList(getSearchEngines())); searchTextField.getSelectButton().setLabelProvider(getSearchEngineLabelProvider()); searchTextField.getSelectButton().addPropertyChangeListener(SelectButton.SELECTED_VALUE, new PropertyChangeListener() { @Override public void propertyChange(PropertyChangeEvent evt) { new SwingWorker<Collection<String>, Void>() { private final S engine = searchTextField.getSelectButton().getSelectedValue(); @Override protected Collection<String> doInBackground() throws Exception { TreeSet<String> set = new TreeSet<String>(String.CASE_INSENSITIVE_ORDER); set.addAll(getHistory(engine)); return set; } @Override protected void done() { if (engine == searchTextField.getSelectButton().getSelectedValue()) { try { searchHistory.clear(); searchHistory.addAll(get()); } catch (Exception e) { debug.log(Level.WARNING, e.getMessage(), e); } } }; }.execute(); } }); try { // restore selected subtitle client searchTextField.getSelectButton().setSelectedIndex(Integer.parseInt(getSettings().get("engine.selected", "0"))); } catch (Exception e) { // log and ignore debug.log(Level.WARNING, e.getMessage(), e); } // save selected client on change searchTextField.getSelectButton().getSelectionModel().addChangeListener(new ChangeListener() { @Override public void stateChanged(ChangeEvent e) { getSettings().put("engine.selected", Integer.toString(searchTextField.getSelectButton().getSelectedIndex())); } }); // high-performance auto-completion AutoCompleteSupport<String> acs = AutoCompleteSupport.install(searchTextField.getEditor(), searchHistory); acs.setTextMatchingStrategy(TextMatcherEditor.IDENTICAL_STRATEGY); acs.setFilterMode(TextMatcherEditor.CONTAINS); acs.setCorrectsCase(true); acs.setStrict(false); installAction(this, KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), searchAction); } protected abstract Collection<String> getHistory(S engine) throws Exception; protected abstract S[] getSearchEngines(); protected abstract LabelProvider<S> getSearchEngineLabelProvider(); protected abstract Settings getSettings(); protected abstract RequestProcessor<?, E> createRequestProcessor(); private void search(RequestProcessor<?, E> requestProcessor) { FileBotTab<?> tab = requestProcessor.tab; tab.setTitle(requestProcessor.getTitle()); tab.setLoading(true); tab.setIcon(requestProcessor.getIcon()); tab.addTo(tabbedPane); tabbedPane.setSelectedComponent(tab); // search in background new SearchTask(requestProcessor).execute(); } private final AbstractAction searchAction = new AbstractAction("Find", ResourceManager.getIcon("action.find")) { @Override public void actionPerformed(ActionEvent e) { if (e.getActionCommand() == null || searchTextField.getText().trim().isEmpty()) { // command triggered by auto-completion return; } RequestProcessor<?, E> request = createRequestProcessor(); if (request != null) { search(request); } } }; private class SearchTask extends SwingWorker<Collection<? extends SearchResult>, Void> { private final RequestProcessor<?, E> requestProcessor; public SearchTask(RequestProcessor<?, E> requestProcessor) { this.requestProcessor = requestProcessor; } @Override protected Collection<? extends SearchResult> doInBackground() throws Exception { long start = System.currentTimeMillis(); try { return requestProcessor.search(); } finally { requestProcessor.duration += (System.currentTimeMillis() - start); } } @Override public void done() { FileBotTab<?> tab = requestProcessor.tab; // tab might have been closed if (tab.isClosed()) return; try { Collection<? extends SearchResult> results = get(); SearchResult selectedSearchResult = null; switch (results.size()) { case 0: log.log(Level.WARNING, String.format("'%s' has not been found.", requestProcessor.request.getSearchText())); break; case 1: selectedSearchResult = results.iterator().next(); break; default: selectedSearchResult = requestProcessor.selectSearchResult(results, SwingUtilities.getWindowAncestor(AbstractSearchPanel.this)); break; } if (selectedSearchResult == null) { tab.close(); return; } // set search result requestProcessor.setSearchResult(selectedSearchResult); tab.setTitle(requestProcessor.getTitle()); // fetch elements of the selected search result new FetchTask(requestProcessor).execute(); } catch (Exception e) { tab.close(); log.log(Level.WARNING, ExceptionUtilities.getRootCauseMessage(e), e); } } } private class FetchTask extends SwingWorker<Collection<E>, Void> { private final RequestProcessor<?, E> requestProcessor; public FetchTask(RequestProcessor<?, E> requestProcessor) { this.requestProcessor = requestProcessor; } @Override protected final Collection<E> doInBackground() throws Exception { long start = System.currentTimeMillis(); try { return requestProcessor.fetch(); } finally { requestProcessor.duration += (System.currentTimeMillis() - start); } } @Override public void done() { FileBotTab<?> tab = requestProcessor.tab; if (tab.isClosed()) return; try { // check if an exception occurred Collection<E> elements = get(); requestProcessor.process(elements); String title = requestProcessor.getTitle(); Icon icon = requestProcessor.getIcon(); String statusMessage = requestProcessor.getStatusMessage(elements); historyPanel.add(title, requestProcessor.getLink(), icon, statusMessage, String.format("%,d ms", requestProcessor.getDuration())); // close tab if no elements were fetched if (get().size() <= 0) { log.warning(statusMessage); tab.close(); } } catch (Exception e) { tab.close(); log.log(Level.WARNING, ExceptionUtilities.getRootCauseMessage(e), e); } finally { tab.setLoading(false); } } } protected static class Request { private final String searchText; public Request(String searchText) { this.searchText = searchText; } public String getSearchText() { return searchText; } } protected abstract static class RequestProcessor<R extends Request, E> { protected final R request; private FileBotTab<JComponent> tab; private SearchResult searchResult; private long duration = 0; public RequestProcessor(R request, JComponent component) { this.request = request; this.tab = new FileBotTab<JComponent>(component); } public abstract Collection<? extends SearchResult> search() throws Exception; public abstract Collection<E> fetch() throws Exception; public abstract void process(Collection<E> elements); public abstract URI getLink(); public JComponent getComponent() { return tab.getComponent(); } public SearchResult getSearchResult() { return searchResult; } public void setSearchResult(SearchResult searchResult) { this.searchResult = searchResult; } public String getStatusMessage(Collection<E> result) { return String.format("%d elements found", result.size()); } public String getTitle() { if (searchResult != null) return searchResult.getName(); return request.getSearchText(); } public Icon getIcon() { return null; } protected SearchResult selectSearchResult(Collection<? extends SearchResult> searchResults, Window window) throws Exception { // multiple results have been found, user must select one SelectDialog<SearchResult> selectDialog = new SelectDialog<SearchResult>(window, searchResults); configureSelectDialog(selectDialog); selectDialog.setVisible(true); // selected value or null if the dialog was canceled by the user return selectDialog.getSelectedValue(); } protected void configureSelectDialog(SelectDialog<SearchResult> selectDialog) { selectDialog.setLocation(getOffsetLocation(selectDialog.getOwner())); selectDialog.setIconImage(getImage(getIcon())); selectDialog.setMinimumSize(new Dimension(250, 150)); selectDialog.pack(); } public long getDuration() { return duration; } } }