blob: 140a9b9ce67cdd955a37c16954a13dfbfe106d37 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2014, 2018 QNX Software Systems and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Alena Laskavaia
*******************************************************************************/
package org.eclipse.launchbar.ui.controls.internal;
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.layout.GridDataFactory;
import org.eclipse.jface.layout.GridLayoutFactory;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerFilter;
import org.eclipse.osgi.util.NLS;
import org.eclipse.swt.SWT;
import org.eclipse.swt.accessibility.AccessibleAdapter;
import org.eclipse.swt.accessibility.AccessibleEvent;
import org.eclipse.swt.events.FocusAdapter;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.progress.WorkbenchJob;
/**
* A simple control that provides a text widget and controls a list viewer
*/
public class FilterControl extends Composite {
/**
* The filter text widget to be used by this tree. This value may be <code>null</code> if there is no filter widget, or if the
* controls have not yet been created.
*/
protected Text filterText;
/**
* The viewer for the filtered tree. This value should never be <code>null</code> after the widget creation methods are
* complete.
*/
protected LaunchBarListViewer listViewer;
protected ViewerFilter patternFilter;
/**
* The text to initially show in the filter text control.
*/
protected String initialText = ""; //$NON-NLS-1$
protected String patternText = null;
/**
* The job used to refresh the tree.
*/
private Job refreshJob;
/**
* The parent composite this control.
*/
protected Composite parent;
/**
* Creates a filter control, to be fully function attachListViewer must be called shortly after
*
* @param parent
*/
public FilterControl(Composite parent) {
super(parent, SWT.NONE);
this.parent = parent;
patternFilter = new ViewerFilter() {
@Override
public boolean select(Viewer viewer, Object parentElement, Object element) {
String text = ((ILabelProvider) listViewer.getLabelProvider()).getText(element);
if (patternText == null)
return true;
String trim = patternText.trim();
if (trim.isEmpty())
return true;
if (text == null)
return false;
if (text.contains(trim))
return true;
if (text.toLowerCase().contains(trim.toLowerCase()))
return true;
return false;
}
};
init();
}
/**
* Create the filtered list.
*/
protected void init() {
createControl(this, SWT.NONE);
createRefreshJob();
setInitialText(Messages.FilterControl_0);
setFont(parent.getFont());
}
/**
* Create the filtered tree's controls. Subclasses should override.
*
* @param parent
* @param treeStyle
*/
protected void createControl(Composite parent, int treeStyle) {
setLayout(GridLayoutFactory.fillDefaults().spacing(0, 0).create());
if (parent.getLayout() instanceof GridLayout) {
setLayoutData(GridDataFactory.fillDefaults().grab(true, true).create());
}
Composite fc = createFilterControls(parent);
fc.setLayoutData(new GridData(SWT.FILL, SWT.BEGINNING, true, false));
}
/**
* Create the filter controls. By default, a text and corresponding tool bar button that clears the contents of the text is
* created. Subclasses may override.
*
* @param parent
* parent <code>Composite</code> of the filter controls
* @return the <code>Composite</code> that contains the filter controls
*/
protected Composite createFilterControls(Composite parent) {
createFilterText(parent);
return parent;
}
public Control attachListViewer(LaunchBarListViewer listViewer) {
this.listViewer = listViewer;
// listViewer.getControl().setLayoutData(GridDataFactory.fillDefaults().grab(true, true).create());
listViewer.getControl().addDisposeListener(e -> refreshJob.cancel());
listViewer.addFilter(patternFilter);
return listViewer.getControl();
}
/**
* Create the refresh job for the receiver.
*
*/
private void createRefreshJob() {
refreshJob = doCreateRefreshJob();
refreshJob.setSystem(true);
}
@Override
public void setVisible(boolean visible) {
boolean oldVisible = getVisible();
if (oldVisible == true && visible == false && listViewer != null && filterText.isFocusControl()) {
listViewer.setFocus();
}
if (getLayoutData() instanceof GridData) {
((GridData) getLayoutData()).heightHint = visible ? SWT.DEFAULT : 0;
}
super.setVisible(visible);
}
/**
* Creates a workbench job that will refresh the tree based on the current filter text. Subclasses may override.
*
* @return a workbench job that can be scheduled to refresh the tree
*
* @since 3.4
*/
protected WorkbenchJob doCreateRefreshJob() {
return new WorkbenchJob("Refresh Filter") {//$NON-NLS-1$
@Override
public IStatus runInUIThread(IProgressMonitor monitor) {
if (listViewer == null)
return Status.CANCEL_STATUS;
if (listViewer.getControl().isDisposed()) {
return Status.CANCEL_STATUS;
}
updatePatternText();
if (patternText == null) {
return Status.OK_STATUS;
}
Control redrawControl = listViewer.getControl();
try {
// don't want the user to see updates that will be made to
// the tree
// we are setting redraw(false) on the composite to avoid
// dancing scrollbar
redrawControl.setRedraw(false);
listViewer.setHistorySupported(patternText == null || patternText.isEmpty());
listViewer.refresh(true);
updateListSelection(false);
// re-focus filterText in case it lost the focus
filterText.setFocus();
} finally {
redrawControl.setRedraw(true);
}
return Status.OK_STATUS;
}
};
}
/**
* Creates the filter text and adds listeners. This method calls {@link #doCreateFilterText(Composite)} to create the text
* control. Subclasses should override {@link #doCreateFilterText(Composite)} instead of overriding this method.
*
* @param parent
* <code>Composite</code> of the filter text
*/
protected void createFilterText(Composite parent) {
filterText = doCreateFilterText(parent);
filterText.getAccessible().addAccessibleListener(new AccessibleAdapter() {
@Override
public void getName(AccessibleEvent e) {
String filterTextString = filterText.getText();
if (filterTextString.length() == 0 || filterTextString.equals(initialText)) {
e.result = initialText;
} else {
e.result = NLS.bind(Messages.FilterControl_1,
new String[] { filterTextString, String.valueOf(getFilteredItemsCount()) });
}
}
/**
* Return the number of filtered items
*
* @return int
*/
private int getFilteredItemsCount() {
return listViewer.getItemCount();
}
});
filterText.addFocusListener(new FocusAdapter() {
@Override
public void focusGained(FocusEvent e) {
}
@Override
public void focusLost(FocusEvent e) {
if (filterText.getText().equals(initialText)) {
setFilterText(""); //$NON-NLS-1$
}
}
});
filterText.addMouseListener(new MouseAdapter() {
@Override
public void mouseDown(MouseEvent e) {
if (filterText.getText().equals(initialText)) {
clearText();
}
}
});
// enter key set focus to tree
filterText.addTraverseListener(e -> {
if (e.detail == SWT.TRAVERSE_RETURN) {
e.doit = false;
listViewer.setFocus();
updateListSelection(true);
} else if (e.detail == SWT.TRAVERSE_ARROW_NEXT) {
listViewer.setFocus();
updateListSelection(false);
} else if (e.detail == SWT.TRAVERSE_ESCAPE) {
listViewer.setDefaultSelection(new StructuredSelection());
e.doit = false;
}
});
filterText.addModifyListener(e -> textChanged());
// if we're using a field with built in cancel we need to listen for
// default selection changes (which tell us the cancel button has been
// pressed)
if ((filterText.getStyle() & SWT.ICON_CANCEL) != 0) {
filterText.addSelectionListener(new SelectionAdapter() {
@Override
public void widgetDefaultSelected(SelectionEvent e) {
if (e.detail == SWT.ICON_CANCEL)
clearText();
}
});
}
filterText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false));
}
protected void updateListSelection(boolean enter) {
if (listViewer.getItemCount() == 0) {
if (enter)
Display.getCurrent().beep();
} else {
StructuredSelection sel;
// if the initial filter text hasn't changed, do not try to match
if (patternText != null && !patternText.trim().isEmpty()) {
// select item with triggering event, it may close the popup if list used as combo
sel = new StructuredSelection(listViewer.getTopFilteredElement());
} else {
sel = new StructuredSelection(listViewer.getTopElement());
}
if (enter)
listViewer.setDefaultSelection(sel);
else
listViewer.setSelection(sel);
}
}
protected Text doCreateFilterText(Composite parent) {
return new Text(parent, SWT.SINGLE | SWT.BORDER | SWT.SEARCH | SWT.ICON_CANCEL);
}
/**
* Update the receiver after the text has changed.
*/
protected void textChanged() {
String old = patternText;
updatePatternText();
if (patternText != null && old == null && patternText.isEmpty())
return;// we changing from initial selection to empty string
if (patternText == old)
return;
// cancel currently running job first, to prevent unnecessary redraw
refreshJob.cancel();
refreshJob.schedule(getRefreshJobDelay());
}
/**
* Return the time delay that should be used when scheduling the filter refresh job. Subclasses may override.
*
* @return a time delay in milliseconds before the job should run
*
*/
protected long getRefreshJobDelay() {
return 200;
}
/**
* Set the background for the widgets that support the filter text area.
*
* @param background
* background <code>Color</code> to set
*/
@Override
public void setBackground(Color background) {
super.setBackground(background);
// listComposite.setBackground(background);
// filterText.setBackground(background);
}
/**
* Clears the text in the filter text widget.
*/
protected void clearText() {
setFilterText(""); //$NON-NLS-1$
// textChanged();
}
/**
* Set the text in the filter control.
*
* @param string
*/
protected void setFilterText(String string) {
if (filterText != null) {
filterText.setText(string);
selectAll();
}
}
public Text getFilterText() {
return filterText;
}
/**
* Get the tree viewer of the receiver.
*
* @return the tree viewer
*/
public LaunchBarListViewer getViewer() {
return listViewer;
}
/**
* Get the filter text for the receiver, if it was created. Otherwise return <code>null</code>.
*
* @return the filter Text, or null if it was not created
*/
public Text getFilterControl() {
return filterText;
}
/**
* Convenience method to return the text of the filter control. If the text widget is not created, then null is returned.
*
* @return String in the text, or null if the text does not exist
*/
protected String getFilterString() {
return filterText != null ? filterText.getText() : null;
}
/**
* Set the text that will be shown until the first focus. A default value is provided, so this method only need be called if
* overriding the default initial text is desired.
*
* @param text
* initial text to appear in text field
*/
public void setInitialText(String text) {
initialText = text;
if (filterText != null) {
filterText.setMessage(text);
if (filterText.isFocusControl()) {
setFilterText(initialText);
textChanged();
} else {
getDisplay().asyncExec(() -> {
if (!filterText.isDisposed() && filterText.isFocusControl()) {
setFilterText(initialText);
textChanged();
}
});
}
} else {
setFilterText(initialText);
textChanged();
}
}
/**
* Select all text in the filter text field.
*
*/
protected void selectAll() {
if (filterText != null) {
filterText.selectAll();
}
}
/**
* Get the initial text for the receiver.
*
* @return String
*/
protected String getInitialText() {
return initialText;
}
private void updatePatternText() {
String text = getFilterString();
boolean initial = initialText != null && initialText.equals(text);
if (initial) {
patternText = null;
} else if (text != null) {
patternText = text;
}
}
}