package org.rubypeople.rdt.internal.ui.infoviews; import java.io.BufferedReader; import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.util.ArrayList; import java.util.Collections; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.StringTokenizer; import java.util.Timer; import java.util.TimerTask; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IAction; import org.eclipse.jface.action.IToolBarManager; import org.eclipse.jface.dialogs.MessageDialog; import org.eclipse.jface.viewers.IStructuredContentProvider; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.viewers.ViewerFilter; import org.eclipse.swt.SWT; import org.eclipse.swt.browser.Browser; import org.eclipse.swt.custom.SashForm; import org.eclipse.swt.events.FocusAdapter; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.events.KeyAdapter; import org.eclipse.swt.events.KeyEvent; import org.eclipse.swt.events.ModifyEvent; import org.eclipse.swt.events.ModifyListener; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.events.SelectionListener; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.Text; import org.eclipse.ui.part.PageBook; import org.eclipse.ui.part.ViewPart; import org.eclipse.ui.progress.UIJob; import org.rubypeople.rdt.internal.ui.RubyPlugin; import org.rubypeople.rdt.internal.ui.RubyPluginImages; import org.rubypeople.rdt.internal.ui.rdocexport.RDocUtility; import org.rubypeople.rdt.internal.ui.rdocexport.RdocListener; import org.rubypeople.rdt.internal.ui.util.CollectionContentProvider; import org.rubypeople.rdt.launching.IVMInstall; import org.rubypeople.rdt.launching.IVMInstallChangedListener; import org.rubypeople.rdt.launching.PropertyChangeEvent; import org.rubypeople.rdt.launching.RubyRuntime; public class RIView extends ViewPart implements RdocListener, IVMInstallChangedListener { private PageBook pageBook; private SashForm form; private Text searchStr; private TableViewer searchListViewer; private Browser searchResult; private static List<String> fgPossibleMatches = new ArrayList<String>(); private IStructuredContentProvider contentProvider = new CollectionContentProvider(); private RubyInvokerJob latestJob; private MyViewerFilter filter; private Timer timer; private Table searchTable; /** * The constructor. */ public RIView() { RubyRuntime.addVMInstallChangedListener(this); } /** * This is a callback that will allow us to create the viewer and initialize * it. */ public void createPartControl(Composite parent) { contributeToActionBars(); pageBook = new PageBook(parent, SWT.NONE); Label inProgressLabel = new Label( pageBook, SWT.LEFT | SWT.TOP | SWT.WRAP ); inProgressLabel.setText(InfoViewMessages.RubyInformation_please_wait); form = new SashForm(pageBook, SWT.HORIZONTAL); Composite panel = new Composite(form, SWT.NONE); panel.setLayout(new GridLayout(1, false)); // Search String timer = new Timer(); searchStr = new Text(panel, SWT.BORDER | SWT.SEARCH); GridData data = new GridData(); data.horizontalAlignment = SWT.FILL; searchStr.setLayoutData(data); searchStr.addModifyListener(new ModifyListener() { public void modifyText(ModifyEvent e) { // run this filter on a timer, reset timer if user hits keystroke before last scheduled filtering starts if (timer != null) timer.cancel(); timer = new Timer(); TimerTask task = new TimerTask() { @Override public void run() { Display.getDefault().asyncExec(new Runnable() { public void run() { filterSearchList(); } }); } }; timer.schedule(task, 500); } }); searchStr.setMessage("Enter class/method name"); // ROR-1329, this method is 3.3+! searchStr.addKeyListener(new KeyAdapter() { public void keyPressed(KeyEvent e) { super.keyPressed(e); if (e.keyCode == 16777218 || e.keyCode == 13) { // sorry didn't find the SWT constant for down arrow searchListViewer.getTable().setFocus(); } else if (e.keyCode == SWT.ESC) { searchStr.setText(""); } } }); searchTable = new Table(panel, SWT.BORDER | SWT.V_SCROLL | SWT.H_SCROLL | SWT.VIRTUAL); searchListViewer = new TableViewer(searchTable); searchListViewer.setContentProvider(contentProvider); data = new GridData(GridData.FILL_VERTICAL | GridData.FILL_HORIZONTAL); searchListViewer.getTable().setLayoutData(data); searchListViewer.getTable().addSelectionListener(new SelectionListener() { public void widgetDefaultSelected(SelectionEvent e) { widgetSelected(e); } public void widgetSelected(SelectionEvent e) { showSelectedItem(); } }); searchStr.addFocusListener(new FocusAdapter() { public void focusGained(FocusEvent e) { searchStr.selectAll(); } }); // Add the filter which narrows our results by what user types filter = new MyViewerFilter(); // search result try { searchResult = new Browser(form, SWT.BORDER); searchResult.setText("<html><body style=\"background-color: #000\"></body></html>"); } catch (Exception e) { MessageDialog.openError(Display.getDefault().getActiveShell(), "Unable to create embedded browser", "It appears that you do not have an embeddable browser. Please see http://www.eclipse.org/swt/faq.php#browserlinux for more information if you are on Linux."); } form.setWeights(new int[]{1, 3}); pageBook.showPage(inProgressLabel); updatePage(); RDocUtility.addRdocListener(this); } private void contributeToActionBars() { IAction refreshAction = new Action() { public void run() { Job job = new Job("Refreshing RI View") { @Override public IStatus run(IProgressMonitor monitor) { RiUtility.rebuildIndex(); updatePage(); return Status.OK_STATUS; } }; job.schedule(); } }; refreshAction.setText(InfoViewMessages.RubyInformation_refresh); refreshAction.setToolTipText(InfoViewMessages.RubyInformation_refresh_tooltip); refreshAction.setImageDescriptor(RubyPluginImages.TOOLBAR_REFRESH); IToolBarManager manager = getViewSite().getActionBars().getToolBarManager(); manager.add(refreshAction); } private void updatePage() { initSearchList(); Display.getDefault().asyncExec(new Runnable () { public void run () { pageBook.showPage(form); } }); } private void showSelectedItem() { String searchText = (String)((IStructuredSelection)searchListViewer.getSelection()).getFirstElement(); if (latestJob != null && latestJob.getState() != Job.NONE) { latestJob.cancel(); } latestJob = new RubyInvokerJob(new RIDescriptionUpdater(searchText)); latestJob.setPriority(Job.INTERACTIVE); latestJob.schedule(); } public void dispose() { RDocUtility.removeRdocListener(this); RubyRuntime.removeVMInstallChangedListener(this); filter = null; super.dispose(); } private synchronized void initSearchList() { RubyInvoker invoker = new RIPopulator(); Job job = new RubyInvokerJob(invoker); job.setPriority(Job.LONG); job.schedule(); } protected List<String> read(Reader reader) { Set<String> results = new HashSet<String>(); BufferedReader reader2 = null; try { reader2 = new BufferedReader(reader); String line = null; while ((line = reader2.readLine()) != null) { results.add(line.trim()); } } catch (IOException e) { RubyPlugin.log(e); } finally { try { if (reader2 != null) reader2.close(); } catch (IOException e) { // ignore } } List<String> list = new ArrayList<String>(results); Collections.sort(list); return list; } private static class RubyInvokerJob extends Job { private RubyInvoker invoker; public RubyInvokerJob(RubyInvoker invoker) { super(InfoViewMessages.RubyInformation_update_job_title); this.invoker = invoker; } @Override protected IStatus run(IProgressMonitor monitor) { invoker.invoke(); return Status.OK_STATUS; } } private void filterSearchList() { UIJob job = new UIJob("Filtering RI List") { @Override public IStatus runInUIThread(IProgressMonitor monitor) { List<String> filtered = filter(searchStr.getText()); searchTable.setItemCount(filtered.size()); searchTable.clearAll(); searchListViewer.setInput(filtered); if (searchTable.getItemCount() > 0) searchTable.setSelection(0); if (searchTable.getItemCount() == 1) showSelectedItem(); return Status.OK_STATUS; } }; job.schedule(); } protected List<String> filter(String text) { filter.setText(text); List<String> filtered = new ArrayList<String>(); for (String possible : fgPossibleMatches) { if (filter.select(null, null, possible)) filtered.add(possible); } return filtered; } /** * Passing the focus request to the viewer's control. */ public void setFocus() { form.setFocus(); } abstract class RubyInvoker { protected abstract List<String> getArgList(); protected abstract void handleOutput(String content); protected void beforeInvoke(){} public abstract void invoke(); } private class RIDescriptionUpdater extends RubyInvoker { private String searchValue; private StringBuilder buffer; RIDescriptionUpdater(String value) { this.searchValue = value; } @Override public void invoke() { String content = RiUtility.getRIHTMLContents(getArgList()); // If we can't find it ourselves then display an error to the // user if (content == null) { content = ""; } handleOutput(content); } protected List<String> getArgList() { List<String> args = new ArrayList<String>(); args.add(searchValue); return args; } protected void beforeInvoke() { searchResult.setText(InfoViewMessages.RubyInformation_please_wait); } protected void handleOutput(final String content) { if (content == null) return; buffer = new StringBuilder(); buffer.append(content); int index = buffer.indexOf("<body>"); buffer.replace(index, index + 6, "<body style=\"color: #fff; background-color: #000\">"); final String text = buffer.toString(); Display.getDefault().syncExec(new Runnable() { public void run() { searchResult.setText(text); } }); } } /** * When the rdoc has changed, automatically update/regenerate the view */ public void rdocChanged() { updatePage(); } private class RIPopulator extends RubyInvoker { @Override public void invoke() { String content = RiUtility.getRIContents(getArgList()); // If we can't find it ourselves then display an error to the // user if (content == null) { content = ""; } handleOutput(content); } @Override protected List<String> getArgList() { List<String> args = new ArrayList<String>(); args.add("--no-pager"); args.add("-l"); return args; } @Override protected void handleOutput(String content) { if (content == null) return; BufferedReader reader = new BufferedReader(new StringReader(content)); String line = null; fgPossibleMatches = read(new StringReader(content)); // if no matches were found display an error message Display.getDefault().asyncExec(new Runnable () { public void run () { searchListViewer.setInput(fgPossibleMatches); filterSearchList(); pageBook.showPage(form); } }); } } public void defaultVMInstallChanged(IVMInstall previous, IVMInstall current) { updatePage(); } public void vmAdded(IVMInstall newVm) { // ignore } public void vmChanged(PropertyChangeEvent event) { // ignore } public void vmRemoved(IVMInstall removedVm) { // ignore } private static class MyViewerFilter extends ViewerFilter { private List<String> userTokens; public void setText(String value) { if (value == null || value.trim().length() == 0) { this.userTokens = null; } else { this.userTokens = getTokens(value); } } @Override public boolean select(Viewer viewer, Object parentElement, Object element) { if (userTokens == null) return true; String riEntry = (String) element; List<String> riListTokens = getTokens(riEntry); if (userTokens.size() == 1) { // special case, match if any token starts with what user typed String userInput = userTokens.get(0); for (int i = 0; i < riListTokens.size(); i++) { if (riListTokens.get(i).startsWith(userInput)) { return true; } } return false; } else { // More than one token if (userTokens.size() > riListTokens.size()) return false; // if user entered longer qualified name, don't even try matching // match up to # of tokens user typed for (int i = 0; i < userTokens.size(); i++) { if (!riListTokens.get(i).startsWith(userTokens.get(i))) { return false; } } return true; } } private List<String> getTokens(String raw) { List<String> tokens = new ArrayList<String>(); StringTokenizer tokenizer = new StringTokenizer(raw, "::#"); while (tokenizer.hasMoreTokens()) { tokens.add(tokenizer.nextToken().toLowerCase()); } return tokens; } } }