/* * Copyright (c) 2009 Andrejs Jermakovics. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Andrejs Jermakovics - initial implementation */ package it.unibz.instasearch.ui; import it.unibz.instasearch.InstaSearchPlugin; import it.unibz.instasearch.actions.CheckUpdatesActionDelegate; import it.unibz.instasearch.actions.ShowExceptionAction; import it.unibz.instasearch.indexing.Field; import it.unibz.instasearch.indexing.SearchQuery; import it.unibz.instasearch.indexing.SearchResultDoc; import it.unibz.instasearch.jobs.CheckUpdatesJob; import it.unibz.instasearch.prefs.PreferenceConstants; import it.unibz.instasearch.ui.ResultContentProvider.MatchLine; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Locale; import org.eclipse.core.runtime.ILogListener; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.IJobChangeEvent; import org.eclipse.core.runtime.jobs.ISchedulingRule; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.core.runtime.jobs.JobChangeAdapter; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.IAction; import org.eclipse.jface.action.IContributionItem; import org.eclipse.jface.action.IMenuListener; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.IToolBarManager; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.text.IFindReplaceTarget; import org.eclipse.jface.util.IPropertyChangeListener; import org.eclipse.jface.util.PropertyChangeEvent; import org.eclipse.jface.viewers.DecoratingStyledCellLabelProvider; import org.eclipse.jface.viewers.DelegatingStyledCellLabelProvider.IStyledLabelProvider; import org.eclipse.jface.viewers.DoubleClickEvent; import org.eclipse.jface.viewers.IBaseLabelProvider; import org.eclipse.jface.viewers.IDoubleClickListener; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.ITreeViewerListener; import org.eclipse.jface.viewers.TreeExpansionEvent; import org.eclipse.jface.viewers.TreeViewer; import org.eclipse.jface.viewers.ViewerCell; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.StyleRange; import org.eclipse.swt.custom.StyledText; 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.MouseEvent; import org.eclipse.swt.events.MouseTrackAdapter; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Menu; import org.eclipse.ui.IEditorPart; import org.eclipse.ui.IViewSite; import org.eclipse.ui.PartInitException; import org.eclipse.ui.part.ViewPart; /** * */ public class InstaSearchView extends ViewPart implements ModifyListener, ILogListener, ITreeViewerListener, IPropertyChangeListener { /** The view ID */ public static final String ID = InstaSearchView.class.getName(); // we have just one view /** Tree for results */ private TreeViewer resultViewer; /** Textbox for search query */ private StyledText searchText; private IAction openAction; private SearchJob searchJob; private ExpandCollapseJob expandCollapseJob; private ResultContentProvider contentProvider; private int lastIncrementalSearchPos = 0; // preferences private int maxResults; private int typingSearchDelay; private boolean incrementalSearchEnabled; private SearchViewControl searchViewControl; /** * */ public InstaSearchView() { } @Override public void init(IViewSite site) throws PartInitException { super.init(site); scheduleUpdateCheck(); // schedule update check if view is opened InstaSearchPlugin.getDefault().getLog().addLogListener(this); // listen for exceptions initPrefs(); InstaSearchPlugin.addPreferenceChangeListener(this); } private void initPrefs() { typingSearchDelay = InstaSearchPlugin.getIntPref(PreferenceConstants.P_TYPING_SEARCH_DELAY); maxResults = InstaSearchPlugin.getIntPref(PreferenceConstants.P_SHOWN_FILES_COUNT); incrementalSearchEnabled = InstaSearchPlugin.getBoolPref(PreferenceConstants.P_INCREMENTAL_SEARCH); } @Override public void createPartControl(Composite parent) { this.searchViewControl = new SearchViewControl(parent, this); searchText = searchViewControl.getSearchText(); resultViewer = searchViewControl.getResultViewer(); searchText.addModifyListener(this); contentProvider = new ResultContentProvider(); IStyledLabelProvider labelProvider = new ResultLabelProvider(contentProvider); IBaseLabelProvider decoratedLabelProvider = new DecoratingStyledCellLabelProvider(labelProvider, null, null); configureResultViewer(contentProvider, decoratedLabelProvider); searchViewControl.setContentProposalAdapter(new SearchContentProposalProvider(contentProvider)); searchJob = new SearchJob(this); expandCollapseJob = new ExpandCollapseJob(); makeActions(); hookContextMenu(); hookDoubleClickAction(); } public void modifyText(ModifyEvent e) { searchJob.cancel(); StyleRange[] styleRanges = createStyledSearchString(searchText.getText()); searchText.setStyleRanges(styleRanges); if( searchViewControl.isShowingSearchTip() ) return; // showing search tip searchJob.schedule(getSearchQuery(), false, typingSearchDelay); // start with a delay since user might still be typing if( incrementalSearchEnabled ) doIncrementalSearch(); } private void doIncrementalSearch() { IEditorPart editor = InstaSearchUI.getActiveEditor(); if( editor != null ) { IFindReplaceTarget target = (IFindReplaceTarget) editor.getAdapter(IFindReplaceTarget.class); if( target != null ) lastIncrementalSearchPos = target.findAndSelect(lastIncrementalSearchPos, searchText.getText(), true, false, false) + searchText.getText().length(); } } /** * Highlight fields * * @param text * @return bold ranges */ private static StyleRange[] createStyledSearchString(String text) { //TODO: use parser ArrayList<StyleRange> styleRanges = new ArrayList<StyleRange>(); String lcaseText = text.toLowerCase(Locale.ENGLISH); ArrayList<String> fieldsToHighlight = new ArrayList<String>(Field.values().length); for(Field field: Field.values()) // add all field names fieldsToHighlight.add(field.toString()); for(String fieldName: fieldsToHighlight) { int pos = lcaseText.indexOf(fieldName + ':'); // should use a parser here while( pos != -1 ) { styleRanges.add(new StyleRange(pos, fieldName.length(), null, null, SWT.BOLD)); pos = lcaseText.indexOf(fieldName + ':', pos+fieldName.length()-1); // find next } } // ranges must be sorted by start position Collections.sort(styleRanges, new Comparator<StyleRange>() { public int compare(StyleRange sr1, StyleRange sr2) { return sr1.start - sr2.start; } }); //TODO: highlight AND, OR return styleRanges.toArray(new StyleRange[styleRanges.size()]); } void setSearchString(String searchString) { searchText.setText(searchString); } /** * Starts the search by giving search string as input to the viewer * * @param searchQuery * @param selectLast whether to select the item which is currently last */ void search(SearchQuery searchQuery, boolean selectLast) { searchJob.cancel(); // cancel previous search searchQuery.setFilter( searchViewControl.getFilter() ); searchJob.schedule(searchQuery, selectLast, 0); } private SearchQuery getSearchQuery() { SearchQuery sq = new SearchQuery(getSearchText(), maxResults ); sq.setFilter( searchViewControl.getFilter() ); return sq; } String getSearchText() { return searchText.getText().trim(); } TreeViewer getResultViewer() { return resultViewer; } public void treeExpanded(TreeExpansionEvent event) { if( event.getElement() instanceof SearchQuery ) { search((SearchQuery)event.getElement(), true); } } public void treeCollapsed(TreeExpansionEvent event) { } private void configureResultViewer(ResultContentProvider contentProvider, IBaseLabelProvider decoratedLabelProvider) { resultViewer.setContentProvider(contentProvider); resultViewer.setLabelProvider(decoratedLabelProvider); resultViewer.setSorter(null); getViewSite().setSelectionProvider(resultViewer); resultViewer.addTreeListener(this); resultViewer.getControl().addMouseTrackListener(new MouseTrackAdapter() { public void mouseHover(MouseEvent e) { ViewerCell cell = resultViewer.getCell(new Point(e.x, e.y)); if( cell != null && cell.getElement() instanceof SearchResultDoc ) { SearchResultDoc doc = (SearchResultDoc) cell.getElement(); resultViewer.getTree().setToolTipText(doc.getFilePath()); } else { resultViewer.getTree().setToolTipText(""); } } }); KeyAdapter keyListener = new KeyAdapter() { public void keyReleased(KeyEvent e) { onSearchTextKeyPress(e); } }; resultViewer.getControl().addKeyListener(keyListener); searchText.addKeyListener(keyListener); } private void hookContextMenu() { MenuManager menuMgr = new MenuManager("#PopupMenu"); menuMgr.setRemoveAllWhenShown(true); menuMgr.addMenuListener(new IMenuListener() { public void menuAboutToShow(IMenuManager manager) { fillContextMenu(manager); } }); Menu menu = menuMgr.createContextMenu(resultViewer.getControl()); resultViewer.getControl().setMenu(menu); getSite().registerContextMenu(menuMgr, resultViewer); } private void onSearchTextKeyPress(KeyEvent e) { if( e.keyCode == SWT.F5 ) { refreshSearch(); } if( e.keyCode == SWT.DEL ) { deleteSelectedMatch(); } if( e.keyCode == (int)'j' && (e.stateMask & SWT.CTRL)!=0 ) { doIncrementalSearch(); } else if( e.keyCode == SWT.TAB ) { resultViewer.getTree().setFocus(); } else if( e.keyCode == SWT.ESC ) { if( expandCollapseJob.getState() == Job.RUNNING ) { expandCollapseJob.cancel(); } else { if( searchText.getSelectionText().equals(searchText.getText()) ) { searchText.setText(""); } else { searchText.setFocus(); searchText.selectAll(); } } } else if( e.getSource() == searchText && e.keyCode == SWT.CR && (e.stateMask & SWT.CTRL)!=0 ) { showAllResults(); } } private void fillContextMenu(IMenuManager manager) { boolean haveSelection = ! resultViewer.getSelection().isEmpty(); SearchQuery sq = (SearchQuery) resultViewer.getInput(); openAction.setEnabled( haveSelection ); manager.add(openAction); boolean showingItems = resultViewer.getTree().getItemCount() > 0; Action expandAll = new Action("Expand All", InstaSearchPlugin.getImageDescriptor("expandall")) { public void run() { expandAll(); } }; expandAll.setEnabled( showingItems ); manager.add(expandAll); Action collapseAll = new Action("Collapse All", InstaSearchPlugin.getImageDescriptor("collapseall")) { public void run() { collapseAll(); } }; collapseAll.setEnabled( showingItems ); manager.add(collapseAll); Action refresh = new Action("Refresh") { public void run() { refreshSearch(); } }; refresh.setAccelerator(SWT.F5); manager.add(refresh); Action delete = new Action("Delete Match") { public void run() { deleteSelectedMatch(); } }; delete.setAccelerator(SWT.DEL); manager.add(delete); Action moreResults = new Action("More Results...") { public void run() { showAllResults(); } }; moreResults.setEnabled( showingItems ); manager.add(moreResults); if( sq == null || !sq.isLimited() ) moreResults.setEnabled(false); } private void deleteSelectedMatch() { if( getResultViewer().getSelection() == null ) return; IStructuredSelection selection = (IStructuredSelection)resultViewer.getSelection(); getResultViewer().remove(selection.toArray()); } /** * */ public void showAllResults() { SearchQuery sq = (SearchQuery)resultViewer.getInput(); SearchQuery newSq = new SearchQuery(sq); newSq.setMaxResults(SearchQuery.UNLIMITED_RESULTS); search(newSq, false); } /** * */ public void expandAll() { expandCollapseJob.schedule(true); } /** * */ public void collapseAll() { expandCollapseJob.schedule(false); } private void openSelection() throws Exception { IStructuredSelection selection = (IStructuredSelection)resultViewer.getSelection(); Object obj = selection.getFirstElement(); SearchResultDoc doc = null; MatchLine selectedLineMatches = null; if(obj instanceof SearchResultDoc) { doc = (SearchResultDoc) obj; } else if(obj instanceof MatchLine) { selectedLineMatches = (MatchLine) obj; doc = selectedLineMatches.getResultDoc(); } else if(obj instanceof Exception) { InstaSearchUI.showError((Exception)obj); return; } else if(obj instanceof SearchQuery ) { search( (SearchQuery) obj, true ); return; } else return; new MatchHighlightJob(doc, selectedLineMatches, contentProvider, searchJob, getSite().getPage()).schedule(); } private void hookDoubleClickAction() { resultViewer.addDoubleClickListener(new IDoubleClickListener() { public void doubleClick(DoubleClickEvent event) { openAction.run(); } }); } public void setFocus() { searchText.setFocus(); //searchText.selectAll(); } private void makeActions() { openAction = new Action("Open") { public void run() { try { openSelection(); } catch (Exception e) { InstaSearchPlugin.log(e); } } }; } private void scheduleUpdateCheck() { boolean checkUpdates = InstaSearchPlugin.getBoolPref(PreferenceConstants.P_CHECK_UPDATES); if( !checkUpdates ) return; CheckUpdatesJob checkUpdatesJob = new CheckUpdatesJob(); checkUpdatesJob.setSystem(true); checkUpdatesJob.addJobChangeListener(new UpdateJobChangeListener()); checkUpdatesJob.schedule(InstaSearchPlugin.getIntPref(PreferenceConstants.P_UPDATE_CHECK_DELAY)); } /** * Logging an error in the plugin * Create an action that allows reporting it */ public void logging(IStatus status, String plugin) { IMenuManager menuManager = getViewSite().getActionBars().getMenuManager(); IContributionItem item = menuManager.find(ShowExceptionAction.ID); if( item != null ) menuManager.remove(item); ShowExceptionAction action = new ShowExceptionAction(status); action.setText("Report Bug"); menuManager.add(action); } @Override public void dispose() { super.dispose(); if( InstaSearchPlugin.getDefault() != null ) { InstaSearchPlugin.getDefault().getLog().removeLogListener(this); InstaSearchPlugin.removePreferenceChangeListener(this); } } private void refreshSearch() { InstaSearchPlugin.getInstaSearch().updateIndex(); SearchQuery input = (SearchQuery) resultViewer.getInput(); if( input == null ) return; resultViewer.setInput(null); // clear cached search results searchJob.cancel(); searchJob.schedule(input, false, typingSearchDelay); } public void propertyChange(PropertyChangeEvent event) { typingSearchDelay = InstaSearchPlugin.getIntPref(PreferenceConstants.P_TYPING_SEARCH_DELAY); maxResults = InstaSearchPlugin.getIntPref(PreferenceConstants.P_SHOWN_FILES_COUNT); incrementalSearchEnabled = InstaSearchPlugin.getBoolPref(PreferenceConstants.P_INCREMENTAL_SEARCH); } /** * Waits for {@link CheckUpdatesJob} to finish and notifies if update is available * by placing an Update button in the view's toolbar */ private class UpdateJobChangeListener extends JobChangeAdapter { public void done(IJobChangeEvent event) { IStatus status = event.getResult(); if( status.getSeverity() == IStatus.OK ) { boolean updateAvailable = (status.getCode() == CheckUpdatesJob.UPDATE_AVAILABLE_CODE); if( updateAvailable ) { getViewSite().getShell().getDisplay().asyncExec(new Runnable() { public void run() { addUpdateAction(); setTitleToolTip("New version available"); getViewSite().getActionBars().getStatusLineManager().setMessage(getTitleImage(), "New version available"); } }); } } } private void addUpdateAction() { IAction updateAction = CheckUpdatesJob.createUpdateNotificationAction(); updateAction.setImageDescriptor( InstaSearchPlugin.getImageDescriptor("lightbulb") ); IToolBarManager mgr = getViewSite().getActionBars().getToolBarManager(); mgr.add(updateAction); mgr.update(true); IMenuManager menuMgr = getViewSite().getActionBars().getMenuManager(); menuMgr.add(updateAction); IContributionItem checkUpdatesItem = mgr.find(CheckUpdatesActionDelegate.ID); if( checkUpdatesItem != null ) checkUpdatesItem.setVisible(false); // hide Check for Updates action } } /** * Background job that expands/collapses all entries */ private class ExpandCollapseJob extends Job implements ISchedulingRule { /** */ public ExpandCollapseJob() { super("Expand All"); setRule(this); //setUser(true); // listen to searchJob changes. stop expanding on new search searchJob.addJobChangeListener(new JobChangeAdapter() { public void scheduled(IJobChangeEvent event) { cancel(); // new search } public void done(IJobChangeEvent event) { cancel(); // canceled search } }); } public void schedule(boolean expandAll) { this.cancel(); if( !expandAll ) { resultViewer.collapseAll(); return; } if( resultViewer.getTree().getItemCount() == 0 ) { return; } this.schedule(); } protected IStatus run(IProgressMonitor monitor) { Display display = getViewSite().getShell().getDisplay(); Object[] elements = contentProvider.getElements(); monitor.beginTask("InstaSearch Expanding", elements.length); for(int i = 0; i < elements.length && !monitor.isCanceled(); i++) { final Object curDoc = elements[i]; if( curDoc == null ) continue; if( !(curDoc instanceof SearchResultDoc) ) continue; contentProvider.getChildren(curDoc); // get lines from file (they become cached) Runnable expander = new Runnable() { public void run() { resultViewer.setExpandedState(curDoc, true); } }; display.syncExec(expander); // expand in UI thread monitor.worked(1); } monitor.done(); return Status.OK_STATUS; } public boolean contains(ISchedulingRule rule) { return rule.getClass() == this.getClass(); } public boolean isConflicting(ISchedulingRule rule) { return rule.getClass() == this.getClass(); } } }