/* ********************************************************************** ** ** Copyright notice ** ** ** ** (c) 2005-2009 RSSOwl Development Team ** ** http://www.rssowl.org/ ** ** ** ** 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.rssowl.org/legal/epl-v10.html ** ** ** ** A copy is found in the file epl-v10.html and important notices to the ** ** license from the team is found in the textfile LICENSE.txt distributed ** ** in this package. ** ** ** ** This copyright notice MUST APPEAR in all copies of the file! ** ** ** ** Contributors: ** ** RSSOwl Development Team - initial API and implementation ** ** ** ** ********************************************************************** */ package org.rssowl.ui.internal.dialogs; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.SafeRunner; import org.eclipse.jface.action.Action; import org.eclipse.jface.action.GroupMarker; import org.eclipse.jface.action.IAction; import org.eclipse.jface.action.IMenuListener; import org.eclipse.jface.action.IMenuManager; import org.eclipse.jface.action.MenuManager; import org.eclipse.jface.action.Separator; import org.eclipse.jface.action.ToolBarManager; import org.eclipse.jface.dialogs.IDialogConstants; import org.eclipse.jface.dialogs.IDialogSettings; import org.eclipse.jface.dialogs.IMessageProvider; import org.eclipse.jface.dialogs.TitleAreaDialog; import org.eclipse.jface.resource.ImageDescriptor; import org.eclipse.jface.resource.JFaceResources; import org.eclipse.jface.resource.LocalResourceManager; import org.eclipse.jface.util.LocalSelectionTransfer; import org.eclipse.jface.viewers.ColumnViewer; import org.eclipse.jface.viewers.ColumnViewerToolTipSupport; import org.eclipse.jface.viewers.DoubleClickEvent; import org.eclipse.jface.viewers.IDoubleClickListener; import org.eclipse.jface.viewers.ISelection; import org.eclipse.jface.viewers.ISelectionChangedListener; import org.eclipse.jface.viewers.IStructuredContentProvider; import org.eclipse.jface.viewers.IStructuredSelection; import org.eclipse.jface.viewers.SelectionChangedEvent; import org.eclipse.jface.viewers.StructuredSelection; import org.eclipse.jface.viewers.TableViewer; import org.eclipse.jface.viewers.TableViewerColumn; import org.eclipse.jface.viewers.Viewer; import org.eclipse.jface.viewers.ViewerCell; import org.eclipse.jface.viewers.ViewerComparator; import org.eclipse.jface.viewers.ViewerFilter; import org.eclipse.jface.window.SameShellProvider; import org.eclipse.jface.window.ToolTip; import org.eclipse.osgi.util.NLS; import org.eclipse.swt.SWT; import org.eclipse.swt.custom.SashForm; import org.eclipse.swt.dnd.DND; import org.eclipse.swt.dnd.DragSourceEvent; import org.eclipse.swt.dnd.DragSourceListener; import org.eclipse.swt.dnd.TextTransfer; import org.eclipse.swt.dnd.Transfer; import org.eclipse.swt.dnd.URLTransfer; import org.eclipse.swt.events.ControlAdapter; import org.eclipse.swt.events.ControlEvent; import org.eclipse.swt.events.PaintEvent; import org.eclipse.swt.events.PaintListener; import org.eclipse.swt.events.SelectionAdapter; import org.eclipse.swt.events.SelectionEvent; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Cursor; import org.eclipse.swt.graphics.GC; import org.eclipse.swt.graphics.Image; import org.eclipse.swt.graphics.Point; import org.eclipse.swt.graphics.Rectangle; import org.eclipse.swt.layout.GridData; import org.eclipse.swt.layout.GridLayout; import org.eclipse.swt.widgets.Button; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Item; import org.eclipse.swt.widgets.Label; import org.eclipse.swt.widgets.Link; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Menu; import org.eclipse.swt.widgets.MenuItem; import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Table; import org.eclipse.swt.widgets.TableColumn; import org.eclipse.swt.widgets.TableItem; import org.eclipse.ui.IWorkbenchActionConstants; import org.eclipse.ui.IWorkbenchPart; import org.eclipse.ui.IWorkbenchWindow; import org.rssowl.core.Owl; import org.rssowl.core.internal.persist.pref.DefaultPreferences; import org.rssowl.core.persist.IBookMark; import org.rssowl.core.persist.IEntity; import org.rssowl.core.persist.IFolderChild; import org.rssowl.core.persist.ILabel; import org.rssowl.core.persist.IModelFactory; import org.rssowl.core.persist.INews; import org.rssowl.core.persist.INews.State; import org.rssowl.core.persist.INewsBin; import org.rssowl.core.persist.ISearchCondition; import org.rssowl.core.persist.ISearchField; import org.rssowl.core.persist.ISearchMark; import org.rssowl.core.persist.SearchSpecifier; import org.rssowl.core.persist.dao.DynamicDAO; import org.rssowl.core.persist.dao.INewsDAO; import org.rssowl.core.persist.event.LabelAdapter; import org.rssowl.core.persist.event.LabelEvent; import org.rssowl.core.persist.event.NewsEvent; import org.rssowl.core.persist.event.NewsListener; import org.rssowl.core.persist.pref.IPreferenceScope; import org.rssowl.core.persist.reference.NewsReference; import org.rssowl.core.persist.service.IModelSearch; import org.rssowl.core.persist.service.PersistenceException; import org.rssowl.core.util.CoreUtils; import org.rssowl.core.util.DateUtils; import org.rssowl.core.util.LoggingSafeRunnable; import org.rssowl.core.util.Pair; import org.rssowl.core.util.SearchHit; import org.rssowl.core.util.StringUtils; import org.rssowl.core.util.URIUtils; import org.rssowl.ui.internal.Activator; import org.rssowl.ui.internal.Application; import org.rssowl.ui.internal.ApplicationActionBarAdvisor; import org.rssowl.ui.internal.ApplicationWorkbenchWindowAdvisor; import org.rssowl.ui.internal.ContextMenuCreator; import org.rssowl.ui.internal.Controller; import org.rssowl.ui.internal.EntityGroup; import org.rssowl.ui.internal.OwlUI; import org.rssowl.ui.internal.actions.ArchiveNewsAction; import org.rssowl.ui.internal.actions.AutomateFilterAction; import org.rssowl.ui.internal.actions.CreateFilterAction.PresetAction; import org.rssowl.ui.internal.actions.MakeNewsStickyAction; import org.rssowl.ui.internal.actions.MoveCopyNewsToBinAction; import org.rssowl.ui.internal.actions.OpenInExternalBrowserAction; import org.rssowl.ui.internal.actions.OpenNewsAction; import org.rssowl.ui.internal.actions.ToggleReadStateAction; import org.rssowl.ui.internal.editors.feed.NewsBrowserLabelProvider; import org.rssowl.ui.internal.editors.feed.NewsBrowserViewer; import org.rssowl.ui.internal.editors.feed.NewsColumn; import org.rssowl.ui.internal.editors.feed.NewsColumnViewModel; import org.rssowl.ui.internal.editors.feed.NewsComparator; import org.rssowl.ui.internal.editors.feed.NewsTableLabelProvider; import org.rssowl.ui.internal.search.LocationControl; import org.rssowl.ui.internal.search.SearchConditionList; import org.rssowl.ui.internal.undo.NewsStateOperation; import org.rssowl.ui.internal.undo.UndoStack; import org.rssowl.ui.internal.util.CTable; import org.rssowl.ui.internal.util.JobRunner; import org.rssowl.ui.internal.util.LayoutUtils; import org.rssowl.ui.internal.util.ModelUtils; import org.rssowl.ui.internal.util.UIBackgroundJob; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Comparator; import java.util.Date; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; /** * The <code>SearchNewsDialog</code> allows to define a number of * <code>ISearchCondition</code>s to search in all News. The result is given out * in a Table-Control below. * * @author bpasero */ public class SearchNewsDialog extends TitleAreaDialog { /* Min width of the dialog in DLUs */ private static final int DIALOG_MIN_WIDTH = 500; /* Sash Weights when Preview is invisible */ private static final int[] TWO_SASH_WEIGHTS = new int[] { 40, 60, 0 }; /* Sash Weights when Preview is visible */ private static final int[] THREE_SASH_WEIGHTS = new int[] { 33, 33, 33 }; /* Section for Dialogs Settings */ private static final String SETTINGS_SECTION = "org.rssowl.ui.internal.dialogs.SearchNewsDialog"; //$NON-NLS-1$ /* Preference: Sash Weights */ private static final String PREF_SASH_WEIGHTS = "org.rssowl.ui.internal.dialogs.search.SashWeights"; //$NON-NLS-1$ /* Columns Action */ private static final String COLUMNS_ACTION = "org.rssowl.ui.internal.dialogs.search.ColumnsAction"; //$NON-NLS-1$ /* Searches Action */ private static final String SEARCHES_ACTION = "org.rssowl.ui.internal.dialogs.search.SearchesAction"; //$NON-NLS-1$ /* Number of News to preload before showing as result */ private static final int NUM_PRELOADED = 20; /* Count number of open Dialogs */ private static int fgOpenDialogCount; /* Button IDs */ private static final int BUTTON_SEARCH = 1000; private static final int BUTTON_CLEAR = 1001; /* Viewer and Controls */ private Button fMatchAllRadio; private Button fMatchAnyRadio; private LocationControl fLocationControl; private SearchConditionList fSearchConditionList; private CTable fCustomTable; private TableViewer fResultViewer; private NewsColumnViewModel fColumnModel; private ScoredNewsComparator fNewsSorter; private Link fStatusLabel; private NewsBrowserViewer fBrowserViewer; private NewsTableLabelProvider fNewsTableLabelProvider; private int[] fCachedWeights; private boolean fUseLowScoreFilter; private AtomicInteger fLowScoreNewsFilteredCount = new AtomicInteger(0); /* Misc. */ private LocalResourceManager fResources; private IDialogSettings fDialogSettings; private IModelSearch fModelSearch; private NewsListener fNewsListener; private boolean fFirstTimeOpen; private boolean fShowsHandCursor; private Cursor fHandCursor; private ISearchCondition fInitialScope; private List<ISearchCondition> fInitialConditions; private boolean fRunSearch; private boolean fMatchAllConditions; private INewsDAO fNewsDao; private IPreferenceScope fPreferences; private LabelAdapter fLabelListener; private boolean fIsPreviewVisible; private SashForm fSashForm; private Composite fBottomSash; private List<ISearchCondition> fCurrentSearchConditions; private long fLastColumnActionInvokedMillies; private Menu fAttachmentsMenu; /* Container for a search result */ private static class ScoredNews { private NewsReference fNewsRef; private INews fResolvedNews; private Float fScore; private Relevance fRelevance; private final State fState; ScoredNews(NewsReference newsRef, INews.State state, Float score, Relevance relevance) { fNewsRef = newsRef; fState = state; fScore = score; fRelevance = relevance; } INews getNews() { if (fResolvedNews == null) { fResolvedNews = fNewsRef.resolve(); if (fResolvedNews == null || !fResolvedNews.isVisible()) CoreUtils.reportIndexIssue(); } return fResolvedNews; } INews.State getState() { return fState; } NewsReference getNewsReference() { return fNewsRef; } Float getScore() { return fScore; } Relevance getRelevance() { return fRelevance; } } /* ScoredNews Relevance */ private enum Relevance { /** Indicates Low Relevance */ LOW, /** Indicates Medium Relevance */ MEDIUM, /** Indicates High Relevance */ HIGH; } /* Comparator for Scored News */ private static class ScoredNewsComparator extends ViewerComparator implements Comparator<ScoredNews> { private NewsComparator fNewsComparator = new NewsComparator(); /* * @see org.eclipse.jface.viewers.ViewerComparator#compare(org.eclipse.jface.viewers.Viewer, * java.lang.Object, java.lang.Object) */ @Override public int compare(Viewer viewer, Object e1, Object e2) { /* Unlikely to happen */ if (!(e1 instanceof ScoredNews) || !(e2 instanceof ScoredNews)) return 0; /* Proceed comparing Scored News */ return compare((ScoredNews) e1, (ScoredNews) e2); } /* * @see java.util.Comparator#compare(java.lang.Object, java.lang.Object) */ public int compare(ScoredNews news1, ScoredNews news2) { /* Not sorting by Score */ if (fNewsComparator.getSortBy() != NewsColumn.RELEVANCE) return fNewsComparator.compare(news1.getNews(), news2.getNews()); /* Sort by Score */ if (!news1.getScore().equals(news2.getScore())) { int result = news1.getScore().compareTo(news2.getScore()); return fNewsComparator.isAscending() ? result : result * -1; } /* Default: Sort by Date */ Date date1 = DateUtils.getRecentDate(news1.getNews()); Date date2 = DateUtils.getRecentDate(news2.getNews()); return date2.compareTo(date1); } void setAscending(boolean ascending) { fNewsComparator.setAscending(ascending); } void setSortBy(NewsColumn sortColumn) { fNewsComparator.setSortBy(sortColumn); } NewsColumn getSortBy() { return fNewsComparator.getSortBy(); } boolean isAscending() { return fNewsComparator.isAscending(); } } /* Filters out Low Score Hits for the first search running */ private class FirstTimeLowScoreFilter extends ViewerFilter { private final AtomicBoolean fEnabled = new AtomicBoolean(true); /* * @see org.eclipse.jface.viewers.ViewerFilter#filter(org.eclipse.jface.viewers.Viewer, java.lang.Object, java.lang.Object[]) */ @Override public Object[] filter(Viewer viewer, Object parent, Object[] elements) { Object[] result = elements; if (fEnabled.get()) { result = super.filter(viewer, parent, elements); fEnabled.set(false); } fLowScoreNewsFilteredCount.set(elements.length - result.length); return result; } /* * @see org.eclipse.jface.viewers.ViewerFilter#select(org.eclipse.jface.viewers.Viewer, java.lang.Object, java.lang.Object) */ @Override public boolean select(Viewer viewer, Object parentElement, Object element) { if (fEnabled.get() && element instanceof ScoredNews) { ScoredNews news = (ScoredNews) element; return (news.getRelevance() == Relevance.HIGH || news.getRelevance() == Relevance.MEDIUM); } return true; } } /* LabelProvider for Scored News */ private static class ScoredNewsLabelProvider extends NewsTableLabelProvider { private Image fHighRelevanceIcon; private Image fMediumRelevanceIcon; private Image fLowRelevanceIcon; ScoredNewsLabelProvider(NewsColumnViewModel model) { super(model); createResources(); } private void createResources() { fHighRelevanceIcon = OwlUI.getImage(fResources, "icons/obj16/high.gif"); //$NON-NLS-1$ fMediumRelevanceIcon = OwlUI.getImage(fResources, "icons/obj16/medium.gif"); //$NON-NLS-1$ fLowRelevanceIcon = OwlUI.getImage(fResources, "icons/obj16/low.gif"); //$NON-NLS-1$ } /* * @see org.eclipse.jface.viewers.OwnerDrawLabelProvider#update(org.eclipse.jface.viewers.ViewerCell) */ @Override public void update(ViewerCell cell) { ScoredNews scoredNews = (ScoredNews) cell.getElement(); NewsColumn column = fColumnModel.getColumn(cell.getColumnIndex()); /* Text */ cell.setText(getColumnText(scoredNews.getNews(), column, cell.getColumnIndex())); /* Image */ cell.setImage(getColumnImage(scoredNews, column, cell.getColumnIndex())); /* Font */ cell.setFont(getFont(scoredNews.getNews(), cell.getColumnIndex())); /* Foreground */ Color foreground = getForeground(scoredNews.getNews(), cell.getColumnIndex()); /* This is required to invalidate + redraw the entire TableItem! */ if (!OwlUI.isHighContrast()) { Item item = (Item) cell.getItem(); if (item instanceof TableItem) ((TableItem) cell.getItem()).setForeground(foreground); } /* Background */ if (!OwlUI.isHighContrast()) cell.setBackground(getBackground(scoredNews.getNews(), cell.getColumnIndex())); } /* * @see org.rssowl.ui.internal.editors.feed.NewsTableLabelProvider#getColumnImage(java.lang.Object, org.rssowl.ui.internal.editors.feed.NewsColumn, int) */ @Override protected Image getColumnImage(Object element, NewsColumn column, int colIndex) { /* Score Column */ if (column == NewsColumn.RELEVANCE) { ScoredNews scoredNews = (ScoredNews) element; switch (scoredNews.getRelevance()) { case HIGH: return fHighRelevanceIcon; case MEDIUM: return fMediumRelevanceIcon; case LOW: return fLowRelevanceIcon; } } /* Any other Column */ return super.getColumnImage(((ScoredNews) element).getNews(), column, colIndex); } /* * @see org.eclipse.jface.viewers.CellLabelProvider#getToolTipText(java.lang.Object) */ @Override public String getToolTipText(Object element) { ScoredNews scoredNews = (ScoredNews) element; INews news = scoredNews.getNews(); String feedRef = news.getFeedLinkAsText(); IBookMark bookMark = CoreUtils.getBookMark(feedRef); String name = null; if (bookMark != null) name = bookMark.getName(); else name = feedRef; if (news.getParentId() != 0) { INewsBin bin = DynamicDAO.load(INewsBin.class, news.getParentId()); if (bin != null) { name = NLS.bind(Messages.SearchNewsDialog_BIN_NAME, bin.getName(), name); } } return StringUtils.replaceAll(name, "&", "&&"); //$NON-NLS-1$ //$NON-NLS-2$ } /* * @see org.rssowl.ui.internal.editors.feed.NewsTableLabelProvider#erase(org.eclipse.swt.widgets.Event, * java.lang.Object) */ @Override public void erase(Event event, Object element) { super.erase(event, ((ScoredNews) element).getNews()); } /* * @see org.rssowl.ui.internal.editors.feed.NewsTableLabelProvider#paint(org.eclipse.swt.widgets.Event, * java.lang.Object) */ @Override protected void paint(Event event, Object element) { super.paint(event, ((ScoredNews) element).getNews()); } /* * @see org.rssowl.ui.internal.editors.feed.NewsTableLabelProvider#measure(org.eclipse.swt.widgets.Event, * java.lang.Object) */ @Override protected void measure(Event event, Object element) { super.measure(event, ((ScoredNews) element).getNews()); } } /* Custom Tooltip Support for Feed Column */ private static class FeedColumnToolTipSupport extends ColumnViewerToolTipSupport { FeedColumnToolTipSupport(ColumnViewer viewer, int style) { super(viewer, style, false); } /* * @see org.eclipse.jface.viewers.ColumnViewerToolTipSupport#getToolTipArea(org.eclipse.swt.widgets.Event) */ @Override protected Object getToolTipArea(Event event) { Table table = (Table) event.widget; Point point = new Point(event.x, event.y); TableItem item = table.getItem(point); /* Only valid for Feed Column */ if (item != null) { int feedIndex = indexOf(table, NewsColumn.FEED); if (feedIndex >= 0 && item.getBounds(feedIndex).contains(point)) return super.getToolTipArea(event); } return null; } private static int indexOf(Table table, NewsColumn column) { if (table.isDisposed()) return -1; TableColumn[] columns = table.getColumns(); for (int i = 0; i < columns.length; i++) if (column == columns[i].getData(NewsColumnViewModel.COL_ID)) return i; return -1; } public static void enableFor(ColumnViewer viewer) { new FeedColumnToolTipSupport(viewer, ToolTip.NO_RECREATE); } } /** * @param parentShell */ public SearchNewsDialog(Shell parentShell) { this(parentShell, null, true, false); } /** * @param parentShell * @param searchScope */ public SearchNewsDialog(Shell parentShell, List<IFolderChild> searchScope) { this(parentShell, toSearchConditions(searchScope), true, false); } private static List<ISearchCondition> toSearchConditions(List<IFolderChild> searchScope) { IModelFactory factory = Owl.getModelFactory(); List<ISearchCondition> conditions = new ArrayList<ISearchCondition>(2); /* Add scope as condition if provided */ if (!searchScope.isEmpty()) { ISearchField field = factory.createSearchField(INews.LOCATION, INews.class.getName()); conditions.add(factory.createSearchCondition(field, SearchSpecifier.SCOPE, ModelUtils.toPrimitive(searchScope))); } /* Add default condition as well */ ISearchField field = factory.createSearchField(IEntity.ALL_FIELDS, INews.class.getName()); conditions.add(factory.createSearchCondition(field, SearchSpecifier.CONTAINS_ALL, "")); //$NON-NLS-1$ return conditions; } /** * @param parentShell * @param initialConditions A List of Conditions that should show initially. * @param matchAllConditions If <code>TRUE</code>, require all conditions to * match, <code>FALSE</code> otherwise. * @param runSearch If <code>TRUE</code>, run the search after the dialog * opened. */ public SearchNewsDialog(Shell parentShell, List<ISearchCondition> initialConditions, boolean matchAllConditions, boolean runSearch) { super(parentShell); fPreferences = Owl.getPreferenceService().getGlobalScope(); fResources = new LocalResourceManager(JFaceResources.getResources()); fDialogSettings = Activator.getDefault().getDialogSettings(); fFirstTimeOpen = (fDialogSettings.getSection(SETTINGS_SECTION) == null); fIsPreviewVisible = fPreferences.getBoolean(DefaultPreferences.SEARCH_DIALOG_PREVIEW_VISIBLE); fCachedWeights = fPreferences.getIntegers(PREF_SASH_WEIGHTS); fModelSearch = Owl.getPersistenceService().getModelSearch(); fHandCursor = parentShell.getDisplay().getSystemCursor(SWT.CURSOR_HAND); fMatchAllConditions = matchAllConditions; fRunSearch = runSearch; fNewsDao = DynamicDAO.getDAO(INewsDAO.class); /* Look for initial conditions and scope */ if (initialConditions != null) { Pair<ISearchCondition, List<ISearchCondition>> conditions = CoreUtils.splitScope(initialConditions); fInitialScope = conditions.getFirst(); fInitialConditions = conditions.getSecond(); } } /** * @param useLowScoreFilter if <code>true</code>, filters the results of the * first run by score. */ public void setUseLowScoreFilter(boolean useLowScoreFilter) { fUseLowScoreFilter = useLowScoreFilter; } /* * @see org.eclipse.jface.window.Window#open() */ @Override public int open() { fgOpenDialogCount++; return super.open(); } /* * @see org.eclipse.jface.dialogs.TrayDialog#close() */ @Override public boolean close() { fgOpenDialogCount--; if (fAttachmentsMenu != null) OwlUI.safeDispose(fAttachmentsMenu); /* Store Column Model */ if (!fResultViewer.getTable().isDisposed()) { NewsColumnViewModel model = NewsColumnViewModel.initializeFrom(fResultViewer.getTable()); model.setSortColumn(fNewsSorter.getSortBy()); model.setAscending(fNewsSorter.isAscending()); model.saveTo(fPreferences, true); } /* Store Preferences */ fPreferences.putBoolean(DefaultPreferences.SEARCH_DIALOG_PREVIEW_VISIBLE, fIsPreviewVisible); if (fCachedWeights != null) fPreferences.putIntegers(PREF_SASH_WEIGHTS, fCachedWeights); /* * Workaround for Eclipse Bug 186025: The Virtual Manager is not cleared * when the TableViewer is disposed. Due to the hookListener() call, a * reference to the TableViewer is held in Memory, so we need to explicitly * clear the virtual manager. */ fResultViewer.setItemCount(0); boolean res = super.close(); fResources.dispose(); unregisterListeners(); return res; } /* * @see org.eclipse.jface.window.Window#configureShell(org.eclipse.swt.widgets.Shell) */ @Override protected void configureShell(Shell shell) { super.configureShell(shell); shell.setText(Messages.SearchNewsDialog_SEARCH_NEWS); } /* * @see org.eclipse.jface.dialogs.Dialog#create() */ @Override public void create() { super.create(); /* Perform the search slightly delayed if requested */ if (fRunSearch) { JobRunner.runInUIThread(200, getShell(), new Runnable() { public void run() { onSearch(); } }); } } /* * @see org.eclipse.jface.dialogs.TitleAreaDialog#createDialogArea(org.eclipse.swt.widgets.Composite) */ @Override protected Control createDialogArea(Composite parent) { /* Separator */ new Label(parent, SWT.SEPARATOR | SWT.HORIZONTAL).setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, true, false)); /* Title */ setTitle(Messages.SearchNewsDialog_SEARCH_NEWS); /* Title Image */ setTitleImage(OwlUI.getImage(fResources, "icons/wizban/search.gif")); //$NON-NLS-1$ /* Title Message */ restoreInfoMessage(false); /* Sashform dividing search definition from results */ fSashForm = new SashForm(parent, SWT.VERTICAL | SWT.SMOOTH); fSashForm.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1)); /* Top Area */ Composite topSash = new Composite(fSashForm, SWT.NONE); topSash.setLayout(LayoutUtils.createGridLayout(1, 0, 0, 0, 0, false)); Composite topSashContent = new Composite(topSash, SWT.None); topSashContent.setLayout(LayoutUtils.createGridLayout(2, 0, 0, 0, 0, false)); topSashContent.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); /* Create Condition Controls */ createConditionControls(topSashContent); /* Separator */ new Label(topSashContent, SWT.SEPARATOR | SWT.HORIZONTAL).setLayoutData(new GridData(SWT.FILL, SWT.END, true, false)); /* Create Center Sash */ Composite centerSash = new Composite(fSashForm, SWT.NONE); centerSash.setLayout(LayoutUtils.createGridLayout(1, 0, 0, 0, 0, false)); centerSash.addControlListener(new ControlAdapter() { @Override public void controlResized(ControlEvent e) { fCachedWeights = fSashForm.getWeights(); } }); Composite centerSashContent = new Composite(centerSash, SWT.None); centerSashContent.setLayout(LayoutUtils.createGridLayout(1, 0, 0, 0, 0, false)); centerSashContent.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); /* Create Viewer for Results */ createResultViewer(centerSashContent); /* Create Bottom Sash */ fBottomSash = new Composite(fSashForm, SWT.NONE); fBottomSash.setLayout(LayoutUtils.createGridLayout(1, 0, 0, 0, 0, false)); fBottomSash.setVisible(fIsPreviewVisible); Composite bottomSashContent = new Composite(fBottomSash, SWT.None); bottomSashContent.setLayout(LayoutUtils.createGridLayout(1, 0, 0, 0, 0, false)); bottomSashContent.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); bottomSashContent.setBackground(bottomSashContent.getDisplay().getSystemColor(SWT.COLOR_LIST_BACKGROUND)); /* Separator */ new Label(bottomSashContent, SWT.SEPARATOR | SWT.HORIZONTAL).setLayoutData(new GridData(SWT.FILL, SWT.END, true, false)); /* Create Viewer for News Item */ createBrowserViewer(bottomSashContent); /* Set weight to SashForm */ if (fCachedWeights != null) fSashForm.setWeights(fCachedWeights); else fSashForm.setWeights(fIsPreviewVisible ? THREE_SASH_WEIGHTS : TWO_SASH_WEIGHTS); /* Separator */ new Label(fBottomSash, SWT.SEPARATOR | SWT.HORIZONTAL).setLayoutData(new GridData(SWT.FILL, SWT.END, true, false)); applyDialogFont(fSashForm); return fSashForm; } private void createBrowserViewer(Composite bottomSashContent) { fBrowserViewer = new NewsBrowserViewer(bottomSashContent, SWT.NONE) { @Override protected Collection<String> getHighlightedWords() { return CoreUtils.extractWords(fCurrentSearchConditions); } }; fBrowserViewer.getControl().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); /* Create Content Provider */ fBrowserViewer.setContentProvider(new IStructuredContentProvider() { public Object[] getElements(Object inputElement) { if (inputElement instanceof Object[] && ((Object[]) inputElement).length > 0) inputElement = ((Object[]) inputElement)[0]; if (inputElement instanceof NewsReference) return new Object[] { ((NewsReference) inputElement).resolve() }; return new Object[] { inputElement }; } public void dispose() {} public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {} }); /* Create LabelProvider */ NewsBrowserLabelProvider labelProvider = new NewsBrowserLabelProvider(fBrowserViewer); labelProvider.setShowFooter(false); labelProvider.setForceShowFeedInformation(true); labelProvider.setStripMediaFromNews(!fPreferences.getBoolean(DefaultPreferences.ENABLE_IMAGES), !fPreferences.getBoolean(DefaultPreferences.ENABLE_MEDIA)); fBrowserViewer.setLabelProvider(labelProvider); /* Set input when selection in result viewer changes */ fResultViewer.addSelectionChangedListener(new ISelectionChangedListener() { public void selectionChanged(SelectionChangedEvent event) { IStructuredSelection selection = (IStructuredSelection) event.getSelection(); if (!selection.isEmpty() && fIsPreviewVisible) { fBrowserViewer.setInput(selection.getFirstElement()); hideBrowser(false); } } }); } private void createConditionControls(Composite container) { Composite topControlsContainer = new Composite(container, SWT.None); topControlsContainer.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, true, false, 2, 1)); topControlsContainer.setLayout(LayoutUtils.createGridLayout(5, 10, 3)); /* Radio to select Condition Matching */ fMatchAllRadio = new Button(topControlsContainer, SWT.RADIO); fMatchAllRadio.setText(Messages.SearchNewsDialog_MATCH_ALL); fMatchAllRadio.setSelection(fMatchAllConditions); fMatchAnyRadio = new Button(topControlsContainer, SWT.RADIO); fMatchAnyRadio.setText(Messages.SearchNewsDialog_MATCH_ANY); fMatchAnyRadio.setSelection(!fMatchAllConditions); /* Separator */ Label sep = new Label(topControlsContainer, SWT.SEPARATOR | SWT.VERTICAL); sep.setLayoutData(new GridData(SWT.DEFAULT, 16)); /* Scope */ Composite scopeContainer = new Composite(topControlsContainer, SWT.None); scopeContainer.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, true)); scopeContainer.setLayout(LayoutUtils.createGridLayout(2, 0, 0, 0, 5, false)); ((GridLayout)scopeContainer.getLayout()).marginLeft = 2; Label locationLabel = new Label(scopeContainer, SWT.NONE); locationLabel.setText(Messages.SearchNewsDialog_SEARCH_IN); fLocationControl = new LocationControl(scopeContainer, SWT.WRAP) { @Override protected String getDefaultLabel() { return Messages.SearchNewsDialog_ALL_NEWS; } }; fLocationControl.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, true)); ((GridData) fLocationControl.getLayoutData()).widthHint = 100; fLocationControl.setLayout(LayoutUtils.createGridLayout(1, 0, 0, 0, 0, false)); /* ToolBar to add and select existing saved searches */ final ToolBarManager dialogToolBar = new ToolBarManager(SWT.RIGHT | SWT.FLAT); /* Columns */ IAction columnDropdown = new Action(Messages.SearchNewsDialog_VISIBLE_COLUMNS, IAction.AS_DROP_DOWN_MENU) { @Override public void run() { OwlUI.positionDropDownMenu(this, dialogToolBar); } @Override public ImageDescriptor getImageDescriptor() { return OwlUI.COLUMNS; } @Override public String getId() { return COLUMNS_ACTION; } }; columnDropdown.setMenuCreator(new ContextMenuCreator() { @Override public Menu createMenu(Control parent) { Menu menu = new Menu(parent); MenuItem restoreDefaults = new MenuItem(menu, SWT.None); restoreDefaults.setText(Messages.SearchNewsDialog_RESTORE_DEFAULT_COLUMNS); restoreDefaults.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { NewsColumnViewModel defaultModel = NewsColumnViewModel.createDefault(true); if (!defaultModel.equals(fColumnModel)) showColumns(defaultModel, true); } }); new MenuItem(menu, SWT.SEPARATOR); NewsColumn[] columns = NewsColumn.values(); for (final NewsColumn column : columns) { if (column.isSelectable()) { MenuItem item = new MenuItem(menu, SWT.CHECK); item.setText(column.getName()); if (fColumnModel.contains(column)) item.setSelection(true); item.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { if (fColumnModel.contains(column)) fColumnModel.removeColumn(column); else fColumnModel.addColumn(column); showColumns(fColumnModel, true); } }); } } return menu; } }); dialogToolBar.add(columnDropdown); /* Separator */ dialogToolBar.add(new Separator()); /* Toggle Preview */ final String previewActionId = "org.rssowl.ui.internal.dialogs.search.PreviewAction"; //$NON-NLS-1$ IAction previewAction = new Action(Messages.SearchNewsDialog_PREVIEW_RESULTS, IAction.AS_CHECK_BOX) { @Override public void run() { fIsPreviewVisible = !fIsPreviewVisible; fSashForm.setWeights(fIsPreviewVisible ? THREE_SASH_WEIGHTS : TWO_SASH_WEIGHTS); fBottomSash.setVisible(fIsPreviewVisible); fSashForm.layout(); dialogToolBar.find(previewActionId).update(IAction.TOOL_TIP_TEXT); /* Select and Show News if required */ if (fIsPreviewVisible && fResultViewer.getTable().getItemCount() > 0) { /* Select first News if required */ if (fResultViewer.getSelection().isEmpty()) fResultViewer.getTable().select(0); /* Set input and Focus */ fBrowserViewer.setInput(((IStructuredSelection) fResultViewer.getSelection()).getFirstElement()); hideBrowser(false); fResultViewer.getTable().setFocus(); /* Make sure to show the selection */ fResultViewer.getTable().showSelection(); } } @Override public ImageDescriptor getImageDescriptor() { return OwlUI.getImageDescriptor("icons/etool16/browsermaximized.gif"); //$NON-NLS-1$ } @Override public String getToolTipText() { if (fIsPreviewVisible) return Messages.SearchNewsDialog_HIDE_PREVIEW; return Messages.SearchNewsDialog_SHOW_PREVIEW; } }; previewAction.setId(previewActionId); previewAction.setChecked(fIsPreviewVisible); dialogToolBar.add(previewAction); /* Separator */ dialogToolBar.add(new Separator()); /* Existing Saved Searches */ IAction savedSearches = new Action(Messages.SearchNewsDialog_SHOW_SAVED_SEARCH, IAction.AS_DROP_DOWN_MENU) { @Override public void run() { OwlUI.positionDropDownMenu(this, dialogToolBar); } @Override public ImageDescriptor getImageDescriptor() { return OwlUI.SEARCHMARK; } @Override public String getId() { return SEARCHES_ACTION; } }; savedSearches.setMenuCreator(new ContextMenuCreator() { @Override public Menu createMenu(Control parent) { Collection<ISearchMark> searchMarks = CoreUtils.loadSortedSearchMarks(); Menu menu = new Menu(parent); /* Create new Saved Search */ MenuItem newSavedSearch = new MenuItem(menu, SWT.NONE); newSavedSearch.setText(Messages.SearchNewsDialog_NEW_SAVED_SEARCH); newSavedSearch.setImage(OwlUI.getImage(fResources, "icons/etool16/add.gif")); //$NON-NLS-1$ newSavedSearch.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { onSave(); } }); /* Separator */ if (searchMarks.size() > 0) new MenuItem(menu, SWT.SEPARATOR); /* Show Existing Saved Searches */ for (final ISearchMark searchMark : searchMarks) { MenuItem item = new MenuItem(menu, SWT.None); item.setText(searchMark.getName()); item.setImage(OwlUI.getImage(fResources, OwlUI.SEARCHMARK)); item.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { show(searchMark); } }); } return menu; } }); dialogToolBar.add(savedSearches); dialogToolBar.createControl(topControlsContainer); dialogToolBar.getControl().setLayoutData(new GridData(SWT.END, SWT.CENTER, true, true)); /* Container for Conditions */ final Composite conditionsContainer = new Composite(container, SWT.NONE); conditionsContainer.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1)); conditionsContainer.setLayout(LayoutUtils.createGridLayout(2, 5, 10)); conditionsContainer.setBackground(container.getDisplay().getSystemColor(SWT.COLOR_LIST_BACKGROUND)); conditionsContainer.setBackgroundMode(SWT.INHERIT_FORCE); conditionsContainer.addPaintListener(new PaintListener() { public void paintControl(PaintEvent e) { GC gc = e.gc; Rectangle clArea = conditionsContainer.getClientArea(); gc.setForeground(conditionsContainer.getDisplay().getSystemColor(SWT.COLOR_WIDGET_NORMAL_SHADOW)); gc.drawLine(clArea.x, clArea.y, clArea.x + clArea.width, clArea.y); } }); /* Search Conditions List */ fSearchConditionList = new SearchConditionList(conditionsContainer, SWT.None, getDefaultConditions()); fSearchConditionList.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1)); fSearchConditionList.setVisibleItemCount(3); /* Show Initial Scope if present */ if (fInitialScope != null && fInitialScope.getValue() instanceof Long[][]) fLocationControl.select((Long[][]) fInitialScope.getValue()); /* Show Initial Conditions if present */ if (fInitialConditions != null) fSearchConditionList.showConditions(fInitialConditions); /* Focus Input */ int index = 0; if (fInitialConditions != null && fInitialConditions.size() == 2) index = 1; fSearchConditionList.focusInput(index); } /* Show conditions of the given searchmark */ private void show(ISearchMark sm) { /* Match Conditions */ fMatchAllRadio.setSelection(sm.matchAllConditions()); fMatchAnyRadio.setSelection(!sm.matchAllConditions()); Pair<ISearchCondition, List<ISearchCondition>> conditions = CoreUtils.splitScope(sm.getSearchConditions()); /* Show Scope */ Long[][] scope = null; if (conditions.getFirst() != null && conditions.getFirst().getValue() instanceof Long[][]) scope = (Long[][]) conditions.getFirst().getValue(); fLocationControl.select(scope); /* Show Conditions */ fSearchConditionList.showConditions(conditions.getSecond()); /* Unset Warning/Error Message */ restoreInfoMessage(true); /* Layout */ fLocationControl.getParent().getParent().getParent().layout(true, true); } private void restoreInfoMessage(boolean clearError) { if (clearError) setErrorMessage(null); setMessage(Messages.SearchNewsDialog_SEARCH_HELP, IMessageProvider.INFORMATION); } /* * @see org.eclipse.jface.dialogs.TrayDialog#createButtonBar(org.eclipse.swt.widgets.Composite) */ @Override protected Control createButtonBar(Composite parent) { GridLayout layout = new GridLayout(1, false); layout.marginWidth = convertHorizontalDLUsToPixels(IDialogConstants.HORIZONTAL_MARGIN); layout.marginHeight = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_MARGIN); layout.horizontalSpacing = convertHorizontalDLUsToPixels(IDialogConstants.HORIZONTAL_SPACING); layout.verticalSpacing = convertVerticalDLUsToPixels(IDialogConstants.VERTICAL_SPACING); Composite buttonBar = new Composite(parent, SWT.NONE); buttonBar.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, true, false)); buttonBar.setLayout(layout); /* Status Label */ fStatusLabel = new Link(buttonBar, SWT.NONE); fStatusLabel.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, true)); fStatusLabel.setText(""); //$NON-NLS-1$ applyDialogFont(fStatusLabel); fStatusLabel.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { onSave(); } }); /* Search */ Button searchButton = createButton(buttonBar, BUTTON_SEARCH, Messages.SearchNewsDialog_SEARCH, true); ((GridData) searchButton.getLayoutData()).horizontalAlignment = SWT.END; ((GridData) searchButton.getLayoutData()).grabExcessHorizontalSpace = false; /* Clear */ createButton(buttonBar, BUTTON_CLEAR, Messages.SearchNewsDialog_CLEAR, false); /* Close */ Button closeButton = createButton(buttonBar, IDialogConstants.CLOSE_ID, IDialogConstants.CLOSE_LABEL, false); closeButton.addSelectionListener(new SelectionAdapter() { @Override public void widgetSelected(SelectionEvent e) { close(); } }); return buttonBar; } /* * @see org.eclipse.jface.dialogs.Dialog#buttonPressed(int) */ @Override protected void buttonPressed(int buttonId) { switch (buttonId) { case BUTTON_SEARCH: onSearch(); break; case BUTTON_CLEAR: onClear(); break; } } private void onSearch() { /* Make sure Conditions are provided */ if (fSearchConditionList.isEmpty()) { setErrorMessage(Messages.SearchNewsDialog_SEARCH_DESCRIPTION); fSearchConditionList.focusInput(); return; } /* Create Conditions */ fCurrentSearchConditions = fSearchConditionList.createConditions(); ISearchCondition locationCondition = fLocationControl.toScopeCondition(); if (locationCondition != null) fCurrentSearchConditions.add(locationCondition); /* Make sure there is no Location Conflict */ if (CoreUtils.isLocationConflict(fCurrentSearchConditions)) { setErrorMessage(null); setMessage(Messages.SearchNewsDialog_LOCATION_WARNING, IMessageProvider.WARNING); } /* Unset Warning/Error Message */ else restoreInfoMessage(true); final boolean matchAllConditions = fMatchAllRadio.getSelection(); /* Disable Buttons and update Cursor */ getButton(BUTTON_SEARCH).setEnabled(false); getShell().setCursor(getShell().getDisplay().getSystemCursor(SWT.CURSOR_APPSTARTING)); JobRunner.runUIUpdater(new UIBackgroundJob(getShell()) { private List<ScoredNews> fResult = null; private Exception fException = null; @Override protected void runInBackground(IProgressMonitor monitor) { /* Perform Search in the Background */ try { List<SearchHit<NewsReference>> searchHits = fModelSearch.searchNews(fCurrentSearchConditions, matchAllConditions); fResult = new ArrayList<ScoredNews>(searchHits.size()); /* Retrieve maximum raw relevance */ Float maxRelevanceScore = 0f; for (SearchHit<NewsReference> searchHit : searchHits) { Float relevanceRaw = searchHit.getRelevance(); maxRelevanceScore = Math.max(maxRelevanceScore, relevanceRaw); } /* Calculate Thresholds */ Float mediumRelThreshold = maxRelevanceScore / 3f * 1f; Float highRelThreshold = maxRelevanceScore / 3f * 2f; Set<State> visibleStates = State.getVisible(); /* Fill Results with Relevance */ for (SearchHit<NewsReference> searchHit : searchHits) { /* Only add visible News for now */ INews.State state = (State) searchHit.getData(INews.STATE); if (!visibleStates.contains(state)) continue; /* Have to test if Entity really exists (bug 337) */ if (!fNewsDao.exists(searchHit.getResult().getId())) { CoreUtils.reportIndexIssue(); continue; } Float relevanceRaw = searchHit.getRelevance(); Relevance relevance = Relevance.LOW; if (relevanceRaw > highRelThreshold) relevance = Relevance.HIGH; else if (relevanceRaw > mediumRelThreshold) relevance = Relevance.MEDIUM; /* Add to result */ fResult.add(new ScoredNews(searchHit.getResult(), state, relevanceRaw, relevance)); } /* Preload some results that are known to be shown initially */ preload(fResult); } catch (PersistenceException e) { fException = e; } } @Override protected void runInUI(IProgressMonitor monitor) { /* Check for error first */ if (fException != null) { setErrorMessage(fException.getMessage()); fResult = Collections.emptyList(); } /* Set Input (sorted) to Viewer */ fResultViewer.setInput(fResult); /* Update Status Label */ String text; int size = fResult.size() - fLowScoreNewsFilteredCount.get(); if (fLowScoreNewsFilteredCount.get() != 0) { if (size == 0) text = NLS.bind(Messages.SearchNewsDialog_SEARCH_RESULT_1_FILTERED, fLowScoreNewsFilteredCount.get()); else if (size == 1) text = NLS.bind(Messages.SearchNewsDialog_SEARCH_RESULT_2_FILTERED, size, fLowScoreNewsFilteredCount.get()); else text = NLS.bind(Messages.SearchNewsDialog_SEARCH_RESULT_3_FILTERED, size, fLowScoreNewsFilteredCount.get()); } else { if (size == 0) text = Messages.SearchNewsDialog_SEARCH_RESULT_1; else if (size == 1) text = NLS.bind(Messages.SearchNewsDialog_SEARCH_RESULT_2, fResult.size()); else text = NLS.bind(Messages.SearchNewsDialog_SEARCH_RESULT_3, fResult.size()); } fStatusLabel.setText(text); /* Enable Buttons and update Cursor */ getButton(BUTTON_SEARCH).setEnabled(true); getShell().setCursor(null); getShell().setDefaultButton(getButton(BUTTON_SEARCH)); getButton(BUTTON_SEARCH).setFocus(); /* Move Focus back to last Search Condition Element */ fSearchConditionList.focusInput(); /* Select First Result if Preview is visible */ if (fIsPreviewVisible && size > 0) { fResultViewer.getTable().select(0); fResultViewer.getTable().showSelection(); /* Set input and Focus */ Object selection = ((IStructuredSelection) fResultViewer.getSelection()).getFirstElement(); boolean refresh = selection.equals(fBrowserViewer.getInput()); fBrowserViewer.setInput(selection); hideBrowser(false); fResultViewer.getTable().setFocus(); if (refresh) fBrowserViewer.refresh(); } /* Clear Browser Viewer otherwise */ else if (fIsPreviewVisible) hideBrowser(true); } }); } private void preload(List<ScoredNews> list) { for (int i = 0; i < list.size() && i < NUM_PRELOADED; i++) { list.get(i).getNews(); } } private void hideBrowser(boolean hide) { if (hide) { fBrowserViewer.setInput(URIUtils.ABOUT_BLANK); fBrowserViewer.getControl().setVisible(false); } else fBrowserViewer.getControl().setVisible(true); } private void onClear() { /* Reset Conditions */ fSearchConditionList.reset(); fMatchAllRadio.setSelection(true); fMatchAnyRadio.setSelection(false); fResultViewer.setInput(Collections.emptyList()); hideBrowser(true); /* Unset Warning/Error Message */ restoreInfoMessage(true); /* Unset Status Message */ fStatusLabel.setText(""); //$NON-NLS-1$ } private void onSave() { List<ISearchCondition> conditions = fSearchConditionList.createConditions(); /* Add default if empty */ if (conditions.isEmpty()) conditions.addAll(getDefaultConditions()); ISearchCondition locationCondition = fLocationControl.toScopeCondition(); if (locationCondition != null) conditions.add(locationCondition); SearchMarkDialog dialog = new SearchMarkDialog((Shell) getShell().getParent(), OwlUI.getBookMarkExplorerSelection(), null, conditions, fMatchAllRadio.getSelection()); dialog.open(); } private void createResultViewer(Composite centerSashContent) { /* Container for Table */ Composite tableContainer = new Composite(centerSashContent, SWT.NONE); tableContainer.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); tableContainer.setLayout(LayoutUtils.createGridLayout(1, 0, 0)); /* Custom Table */ int style = SWT.MULTI | SWT.FULL_SELECTION | SWT.VIRTUAL; fCustomTable = new CTable(tableContainer, style); /* Viewer */ fResultViewer = new TableViewer(fCustomTable.getControl()) { @Override public ISelection getSelection() { StructuredSelection selection = (StructuredSelection) super.getSelection(); return convertToNews(selection); } }; fResultViewer.getControl().setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true)); fResultViewer.setUseHashlookup(true); fResultViewer.getControl().setData(ApplicationWorkbenchWindowAdvisor.FOCUSLESS_SCROLL_HOOK, new Object()); fResultViewer.getTable().setHeaderVisible(true); /* Custom Tooltips for Feed Column */ FeedColumnToolTipSupport.enableFor(fResultViewer); /* Separator */ new Label(centerSashContent, SWT.SEPARATOR | SWT.HORIZONTAL).setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, true, false)); /* Apply ContentProvider */ fResultViewer.setContentProvider(getContentProvider()); /* Create LabelProvider */ NewsColumnViewModel model = NewsColumnViewModel.loadFrom(fPreferences, true); fNewsTableLabelProvider = new ScoredNewsLabelProvider(model); if (!OwlUI.isHighContrast()) { fResultViewer.getControl().addListener(SWT.EraseItem, new Listener() { public void handleEvent(Event event) { Object element = event.item.getData(); fNewsTableLabelProvider.erase(event, element); } }); } /* Create Sorter */ fNewsSorter = new ScoredNewsComparator(); fResultViewer.setComparator(fNewsSorter); /* Create Filter (if necessary) */ if (fUseLowScoreFilter) fResultViewer.addFilter(new FirstTimeLowScoreFilter()); /* Create the Columns */ showColumns(model, false); /* Hook Contextual Menu */ hookContextualMenu(); /* Drag and Drop */ initDragAndDrop(); /* Register Listeners */ registerListeners(); } private void showColumns(NewsColumnViewModel model, boolean update) { fResultViewer.getTable().setRedraw(false); try { /* Dispose Old */ fCustomTable.clear(); /* Keep as current */ fColumnModel = model; /* Create New */ List<NewsColumn> cols = model.getColumns(); for (NewsColumn col : cols) { TableViewerColumn viewerColumn = new TableViewerColumn(fResultViewer, SWT.LEFT); fCustomTable.manageColumn(viewerColumn.getColumn(), model.getLayoutData(col), col.showName() ? col.getName() : null, col.showTooltip() ? col.getName() : null, null, col.isMoveable(), col.isResizable()); viewerColumn.getColumn().setData(NewsColumnViewModel.COL_ID, col); if (model.getSortColumn() == col && col.showSortIndicator()) { fCustomTable.getControl().setSortColumn(viewerColumn.getColumn()); fCustomTable.getControl().setSortDirection(model.isAscending() ? SWT.UP : SWT.DOWN); } } /* Enable Sorting adding listeners to Columns */ TableColumn[] columns = fResultViewer.getTable().getColumns(); for (final TableColumn column : columns) { column.addSelectionListener(new SelectionAdapter() { @SuppressWarnings("unchecked") @Override public void widgetSelected(SelectionEvent e) { NewsColumn oldSortBy = fNewsSorter.getSortBy(); NewsColumn newSortBy = (NewsColumn) column.getData(NewsColumnViewModel.COL_ID); boolean defaultAscending = newSortBy.prefersAscending(); boolean ascending = (oldSortBy != newSortBy) ? defaultAscending : !fNewsSorter.isAscending(); fNewsSorter.setSortBy(newSortBy); fNewsSorter.setAscending(ascending); fColumnModel.setSortColumn(newSortBy); fColumnModel.setAscending(ascending); /* Indicate Sort-Column in UI for Columns that have a certain width */ if (newSortBy.showSortIndicator()) { fResultViewer.getTable().setSortColumn(column); fResultViewer.getTable().setSortDirection(ascending ? SWT.UP : SWT.DOWN); } else { fResultViewer.getTable().setSortColumn(null); } /* Since Virtual Style is set, we have to sort the model manually */ if (fResultViewer.getInput() != null) { Collections.sort(((List<ScoredNews>) fResultViewer.getInput()), fNewsSorter); fResultViewer.refresh(false); } } }); } /* Update Table */ if (update) fCustomTable.update(); /* Update Sorter */ fNewsSorter.setAscending(model.isAscending()); fNewsSorter.setSortBy(model.getSortColumn()); /* Set Label Provider */ fNewsTableLabelProvider.init(model); fResultViewer.setLabelProvider(fNewsTableLabelProvider); /* Refresh if necessary */ if (update) fResultViewer.refresh(true); } finally { fResultViewer.getTable().setRedraw(true); } } private void initDragAndDrop() { int ops = DND.DROP_COPY | DND.DROP_MOVE; Transfer[] transfers = new Transfer[] { LocalSelectionTransfer.getTransfer(), TextTransfer.getInstance(), URLTransfer.getInstance() }; /* Drag Support */ fResultViewer.addDragSupport(ops, transfers, new DragSourceListener() { public void dragStart(final DragSourceEvent event) { SafeRunner.run(new LoggingSafeRunnable() { public void run() throws Exception { LocalSelectionTransfer.getTransfer().setSelection(fResultViewer.getSelection()); LocalSelectionTransfer.getTransfer().setSelectionSetTime(event.time & 0xFFFFFFFFL); event.doit = true; } }); } public void dragSetData(final DragSourceEvent event) { SafeRunner.run(new LoggingSafeRunnable() { public void run() throws Exception { /* Set Selection using LocalSelectionTransfer */ if (LocalSelectionTransfer.getTransfer().isSupportedType(event.dataType)) event.data = LocalSelectionTransfer.getTransfer().getSelection(); /* Set Text using Text- or URLTransfer */ else if (TextTransfer.getInstance().isSupportedType(event.dataType) || URLTransfer.getInstance().isSupportedType(event.dataType)) setTextData(event); } }); } public void dragFinished(DragSourceEvent event) { SafeRunner.run(new LoggingSafeRunnable() { public void run() throws Exception { LocalSelectionTransfer.getTransfer().setSelection(null); LocalSelectionTransfer.getTransfer().setSelectionSetTime(0); } }); } }); } private void setTextData(DragSourceEvent event) { IStructuredSelection selection = (IStructuredSelection) LocalSelectionTransfer.getTransfer().getSelection(); Collection<INews> news = ModelUtils.normalize(selection.toList()); if (!news.isEmpty()) { StringBuilder strB = new StringBuilder(); for (INews item : news) { String link = CoreUtils.getLink(item); if (StringUtils.isSet(link)) { strB.append(link); if (news.size() > 1) strB.append("\n"); //$NON-NLS-1$ } } if (strB.length() > 0) event.data = strB.toString(); } } /* Convert Selection to INews */ private ISelection convertToNews(StructuredSelection selection) { List<?> selectedElements = selection.toList(); List<INews> selectedNews = new ArrayList<INews>(); for (Object selectedElement : selectedElements) { ScoredNews scoredNews = (ScoredNews) selectedElement; selectedNews.add(scoredNews.getNews()); } return new StructuredSelection(selectedNews); } private void registerListeners() { /* Open selected News Links in Browser on doubleclick */ fResultViewer.addDoubleClickListener(new IDoubleClickListener() { public void doubleClick(DoubleClickEvent event) { onMouseDoubleClick(event); } }); /* Perform Action on Mouse-Down */ fResultViewer.getControl().addListener(SWT.MouseDown, new Listener() { public void handleEvent(Event event) { onMouseDown(event); } }); /* Update Cursor on Mouse-Move */ fResultViewer.getControl().addListener(SWT.MouseMove, new Listener() { public void handleEvent(Event event) { onMouseMove(event); } }); /* Listen to News-Events */ fNewsListener = new NewsListener() { public void entitiesAdded(Set<NewsEvent> events) { /* Ignore */ } public void entitiesUpdated(Set<NewsEvent> events) { onNewsEvent(events); } public void entitiesDeleted(Set<NewsEvent> events) { /* Ignore */ } }; DynamicDAO.addEntityListener(INews.class, fNewsListener); /* Redraw on Label update */ fLabelListener = new LabelAdapter() { @Override public void entitiesUpdated(Set<LabelEvent> events) { JobRunner.runInUIThread(fResultViewer.getTable(), new Runnable() { public void run() { fResultViewer.refresh(true); } }); } }; DynamicDAO.addEntityListener(ILabel.class, fLabelListener); } private void onNewsEvent(final Set<NewsEvent> events) { /* No Result set yet */ if (fResultViewer.getInput() == null) return; /* Check for Update / Deleted News */ JobRunner.runUIUpdater(new UIBackgroundJob(getShell()) { private List<ScoredNews> fDeletedScoredNews; private List<ScoredNews> fUpdatedScoredNews; private Set<NewsEvent> fUpdatedNewsEvents; @Override protected void runInBackground(IProgressMonitor monitor) { List<?> input = (List<?>) fResultViewer.getInput(); for (NewsEvent event : events) { for (Object object : input) { ScoredNews scoredNews = ((ScoredNews) object); NewsReference newsRef = scoredNews.getNewsReference(); /* Return on Cancellation or Shutdown */ if (monitor.isCanceled() || Controller.getDefault().isShuttingDown()) return; /* News is part of the list */ if (newsRef.references(event.getEntity())) { INews news = event.getEntity(); /* News got Deleted */ if (!news.isVisible()) { if (fDeletedScoredNews == null) fDeletedScoredNews = new ArrayList<ScoredNews>(); fDeletedScoredNews.add(scoredNews); } /* News got Updated */ else { if (fUpdatedScoredNews == null) fUpdatedScoredNews = new ArrayList<ScoredNews>(); fUpdatedScoredNews.add(scoredNews); if (fUpdatedNewsEvents == null) fUpdatedNewsEvents = new HashSet<NewsEvent>(); fUpdatedNewsEvents.add(event); } } } } } @Override protected void runInUI(IProgressMonitor monitor) { /* Return on Cancellation or Shutdown */ if (monitor.isCanceled() || Controller.getDefault().isShuttingDown()) return; /* News got Deleted */ if (fDeletedScoredNews != null) { /* Temporary Fix for https://bugs.eclipse.org/bugs/show_bug.cgi?id=295980 */ if (Application.isWindows7()) { Object input = fResultViewer.getInput(); if (input instanceof List<?>) { ((List<?>) input).removeAll(fDeletedScoredNews); fResultViewer.refresh(); } } else fResultViewer.remove(fDeletedScoredNews.toArray()); } /* News got Updated */ if (fUpdatedScoredNews != null) fResultViewer.update(fUpdatedScoredNews.toArray(), null); /* Update Browser Viewer if visible */ if (fBrowserViewer.getControl().isVisible()) { Object input = fBrowserViewer.getInput(); if (fUpdatedNewsEvents != null) { for (NewsEvent event : fUpdatedNewsEvents) { if (event.getEntity().equals(input)) { fBrowserViewer.update(Collections.singleton(event)); break; // Viewer only shows 1 News at maximum } } } if (fDeletedScoredNews != null) { for (ScoredNews news : fDeletedScoredNews) { if (news.getNews().equals(input)) { fBrowserViewer.remove(news.getNews()); break; // Viewer only shows 1 News at maximum } } } } } }); } private void onMouseDown(Event event) { Point p = new Point(event.x, event.y); TableItem item = fResultViewer.getTable().getItem(p); /* Problem - return */ if (item == null || item.isDisposed()) return; /* Mouse-Up over Read-State-Column */ if (event.button == 1 && isInImageBounds(item, NewsColumn.TITLE, p)) { Object data = item.getData(); /* Toggle State between Read / Unread */ if (data instanceof ScoredNews) { INews news = ((ScoredNews) data).getNews(); INews.State newState = (news.getState() == INews.State.READ) ? INews.State.UNREAD : INews.State.READ; setNewsState(new ArrayList<INews>(Arrays.asList(new INews[] { news })), newState); fLastColumnActionInvokedMillies = System.currentTimeMillis(); } } /* Mouse-Up over Sticky-State-Column */ else if (event.button == 1 && isInImageBounds(item, NewsColumn.STICKY, p)) { Object data = item.getData(); /* Toggle State between Sticky / Not Sticky */ if (data instanceof ScoredNews) { new MakeNewsStickyAction(new StructuredSelection(((ScoredNews) data).getNews())).run(); fLastColumnActionInvokedMillies = System.currentTimeMillis(); } } /* Mouse-Up over Attachments-Column */ else if (event.button == 1 && isInImageBounds(item, NewsColumn.ATTACHMENTS, p)) { Object data = item.getData(); if (data instanceof ScoredNews) { MenuManager contextMenu = new MenuManager(); ApplicationActionBarAdvisor.fillAttachmentsMenu(contextMenu, new StructuredSelection(((ScoredNews) data).getNews()), this, true); if (fAttachmentsMenu != null) OwlUI.safeDispose(fAttachmentsMenu); fAttachmentsMenu = contextMenu.createContextMenu(fResultViewer.getControl()); Point cursorLocation = item.getDisplay().getCursorLocation(); cursorLocation.y = cursorLocation.y + 16; fAttachmentsMenu.setLocation(cursorLocation); fAttachmentsMenu.setVisible(true); fLastColumnActionInvokedMillies = System.currentTimeMillis(); } } } private void onMouseMove(Event event) { Point p = new Point(event.x, event.y); TableItem item = fResultViewer.getTable().getItem(p); /* Problem / Group hovered - reset */ if (item == null || item.isDisposed() || item.getData() instanceof EntityGroup) { if (fShowsHandCursor && !fResultViewer.getControl().isDisposed()) { fResultViewer.getControl().setCursor(null); fShowsHandCursor = false; } return; } /* Show Hand-Cursor if action can be performed */ boolean changeToHandCursor = isInImageBounds(item, NewsColumn.TITLE, p) || isInImageBounds(item, NewsColumn.STICKY, p) || isInImageBounds(item, NewsColumn.ATTACHMENTS, p); if (!fShowsHandCursor && changeToHandCursor) { fResultViewer.getControl().setCursor(fHandCursor); fShowsHandCursor = true; } else if (fShowsHandCursor && !changeToHandCursor) { fResultViewer.getControl().setCursor(null); fShowsHandCursor = false; } } private void unregisterListeners() { DynamicDAO.removeEntityListener(INews.class, fNewsListener); DynamicDAO.removeEntityListener(ILabel.class, fLabelListener); } private void onMouseDoubleClick(DoubleClickEvent event) { IStructuredSelection selection = (IStructuredSelection) event.getSelection(); if (selection.isEmpty()) return; /* Do nothing if the user recently invokved a column action */ if (System.currentTimeMillis() - fLastColumnActionInvokedMillies > 200) { /* Convert Selection to INews */ List<?> selectedElements = selection.toList(); List<INews> selectedNews = new ArrayList<INews>(); for (Object selectedElement : selectedElements) { ScoredNews scoredNews = (ScoredNews) selectedElement; selectedNews.add(scoredNews.getNews()); } /* Open News */ new OpenNewsAction(new StructuredSelection(selectedNews), getShell()).run(); } } private void hookContextualMenu() { MenuManager manager = new MenuManager(); manager.setRemoveAllWhenShown(true); manager.addMenuListener(new IMenuListener() { public void menuAboutToShow(IMenuManager manager) { final IStructuredSelection selection = (IStructuredSelection) fResultViewer.getSelection(); /* Open */ { /* Open in FeedView */ manager.add(new Separator("internalopen")); //$NON-NLS-1$ if (!selection.isEmpty()) manager.appendToGroup("internalopen", new OpenNewsAction(selection, getShell())); //$NON-NLS-1$ manager.add(new GroupMarker("open")); //$NON-NLS-1$ /* Show only when internal browser is used */ if (!selection.isEmpty() && !OwlUI.useExternalBrowser()) manager.add(new OpenInExternalBrowserAction(selection)); } /* Attachments */ { ApplicationActionBarAdvisor.fillAttachmentsMenu(manager, selection, new SameShellProvider(getShell()), false); } /* Mark / Label */ if (!selection.isEmpty()) { manager.add(new Separator("mark")); //$NON-NLS-1$ /* Mark */ MenuManager markMenu = new MenuManager(Messages.SearchNewsDialog_MARK, "mark"); //$NON-NLS-1$ manager.add(markMenu); /* Mark as Read */ IAction action = new ToggleReadStateAction(selection); action.setEnabled(!selection.isEmpty()); markMenu.add(action); /* Sticky */ markMenu.add(new Separator()); action = new MakeNewsStickyAction(selection); action.setEnabled(!selection.isEmpty()); markMenu.add(action); /* Label */ ApplicationActionBarAdvisor.fillLabelMenu(manager, selection, new SameShellProvider(getShell()), false); } /* Move To / Copy To */ if (!selection.isEmpty()) { manager.add(new Separator("movecopy")); //$NON-NLS-1$ /* Load all news bins and sort by name */ List<INewsBin> newsbins = new ArrayList<INewsBin>(DynamicDAO.loadAll(INewsBin.class)); Comparator<INewsBin> comparator = new Comparator<INewsBin>() { public int compare(INewsBin o1, INewsBin o2) { return o1.getName().compareTo(o2.getName()); }; }; Collections.sort(newsbins, comparator); /* Move To */ MenuManager moveMenu = new MenuManager(Messages.SearchNewsDialog_MOVE, "moveto"); //$NON-NLS-1$ manager.add(moveMenu); for (INewsBin bin : newsbins) { moveMenu.add(new MoveCopyNewsToBinAction(selection, bin, true)); } moveMenu.add(new MoveCopyNewsToBinAction(selection, null, true)); moveMenu.add(new Separator()); moveMenu.add(new AutomateFilterAction(PresetAction.MOVE, selection)); /* Copy To */ MenuManager copyMenu = new MenuManager(Messages.SearchNewsDialog_COPY, "copyto"); //$NON-NLS-1$ manager.add(copyMenu); for (INewsBin bin : newsbins) { copyMenu.add(new MoveCopyNewsToBinAction(selection, bin, false)); } copyMenu.add(new MoveCopyNewsToBinAction(selection, null, false)); copyMenu.add(new Separator()); copyMenu.add(new AutomateFilterAction(PresetAction.COPY, selection)); /* Archive */ manager.add(new ArchiveNewsAction(selection)); } /* Share */ { ApplicationActionBarAdvisor.fillShareMenu(manager, selection, new SameShellProvider(getShell()), false); } manager.add(new Separator("filter")); //$NON-NLS-1$ manager.add(new Separator("copy")); //$NON-NLS-1$ manager.add(new GroupMarker("edit")); //$NON-NLS-1$ manager.add(new Separator(IWorkbenchActionConstants.MB_ADDITIONS)); } }); /* Create and Register with Workbench */ Menu menu = manager.createContextMenu(fResultViewer.getControl()); fResultViewer.getControl().setMenu(menu); /* Register with Part Site */ IWorkbenchWindow window = OwlUI.getWindow(); if (window != null) { IWorkbenchPart activePart = window.getPartService().getActivePart(); if (activePart != null && activePart.getSite() != null) activePart.getSite().registerContextMenu(manager, fResultViewer); } } private IStructuredContentProvider getContentProvider() { return new IStructuredContentProvider() { public Object[] getElements(Object inputElement) { if (inputElement instanceof List<?>) return getVisibleNews((List<?>) inputElement); return new Object[0]; } public void dispose() {} public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {} }; } private Object[] getVisibleNews(List<?> elements) { List<ScoredNews> news = new ArrayList<ScoredNews>(); Set<INews.State> visibleStates = INews.State.getVisible(); for (Object element : elements) { if (element instanceof ScoredNews) { ScoredNews scoredNews = (ScoredNews) element; if (visibleStates.contains(scoredNews.getState())) news.add((ScoredNews) element); } } return news.toArray(); } private List<ISearchCondition> getDefaultConditions() { List<ISearchCondition> conditions = new ArrayList<ISearchCondition>(1); IModelFactory factory = Owl.getModelFactory(); ISearchField field = factory.createSearchField(IEntity.ALL_FIELDS, INews.class.getName()); ISearchCondition condition = factory.createSearchCondition(field, SearchSpecifier.CONTAINS_ALL, ""); //$NON-NLS-1$ conditions.add(condition); return conditions; } /* * @see org.eclipse.jface.window.Window#getShellStyle() */ @Override protected int getShellStyle() { int style = SWT.TITLE | SWT.BORDER | SWT.MIN | SWT.MAX | SWT.RESIZE | SWT.CLOSE | getDefaultOrientation(); return style; } /* * @see org.eclipse.jface.dialogs.Dialog#getDialogBoundsSettings() */ @Override protected IDialogSettings getDialogBoundsSettings() { IDialogSettings section = fDialogSettings.getSection(SETTINGS_SECTION); if (section != null) return section; return fDialogSettings.addNewSection(SETTINGS_SECTION); } /* * @see org.eclipse.jface.dialogs.Dialog#initializeBounds() */ @Override protected void initializeBounds() { super.initializeBounds(); /* No dialog settings stored */ if (fFirstTimeOpen) { /* Minimum Size */ int minWidth = convertHorizontalDLUsToPixels(DIALOG_MIN_WIDTH); Point bestSize = getShell().computeSize(SWT.DEFAULT, SWT.DEFAULT); getShell().setSize(minWidth, bestSize.y); LayoutUtils.positionShell(getShell()); } /* Move a bit to bottom right if multiple dialogs are open at the same time */ if (fgOpenDialogCount > 1) { Point location = getShell().getLocation(); location.x += 20 * (fgOpenDialogCount - 1); location.y += 20 * (fgOpenDialogCount - 1); getShell().setLocation(location); } } private void setNewsState(List<INews> news, INews.State state) { boolean affectEquivalentNews = (state != INews.State.UNREAD && OwlUI.markReadDuplicates()); /* Add to UndoStack */ UndoStack.getInstance().addOperation(new NewsStateOperation(news, state, affectEquivalentNews)); /* Perform Operation */ Owl.getPersistenceService().getDAOService().getNewsDAO().setState(news, state, affectEquivalentNews, false); } private int indexOf(NewsColumn column) { Table table = fCustomTable.getControl(); if (table.isDisposed()) return -1; TableColumn[] columns = table.getColumns(); for (int i = 0; i < columns.length; i++) { if (column == columns[i].getData(NewsColumnViewModel.COL_ID)) return i; } return -1; } private boolean isInImageBounds(TableItem item, NewsColumn column, Point p) { int index = indexOf(column); if (index == -1) return false; return item.getImageBounds(index).contains(p); } }