Skip to content

Commit

Permalink
Implement filter-based table viewer via the new FilterTable class
Browse files Browse the repository at this point in the history
This moves the viewer-agnostic components of the FilterTree widget into
an abstract base class, which is then used to implement the FilterTable
widget.
  • Loading branch information
ptziegler committed Dec 2, 2024
1 parent 6c459cc commit 5ccbcda
Show file tree
Hide file tree
Showing 20 changed files with 1,171 additions and 341 deletions.
2 changes: 1 addition & 1 deletion bundles/org.eclipse.ui.workbench/META-INF/MANIFEST.MF
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ Manifest-Version: 1.0
Bundle-ManifestVersion: 2
Bundle-Name: %pluginName
Bundle-SymbolicName: org.eclipse.ui.workbench; singleton:=true
Bundle-Version: 3.134.100.qualifier
Bundle-Version: 3.135.0.qualifier
Bundle-Activator: org.eclipse.ui.internal.WorkbenchPlugin
Bundle-ActivationPolicy: lazy
Bundle-Vendor: %providerName
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,373 @@
/*******************************************************************************
* Copyright (c) 2024 Patrick Ziegler 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:
* Patrick Ziegler - initial API and implementation
******************************************************************************/
package org.eclipse.ui.dialogs;

import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jface.viewers.ColumnViewer;
import org.eclipse.swt.SWT;
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.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.IWorkbenchPreferenceConstants;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.internal.WorkbenchMessages;
import org.eclipse.ui.progress.WorkbenchJob;

/**
* The abstract base class for a composite containing both a column viewer and a
* text widget. The text drives the filter of the viewer. Current
* implementations support both table- and tree-based viewers.
*
* @since 3.135
*/
public abstract sealed class AbstractFilteredViewer extends Composite permits FilteredTable, FilteredTree {

/**
* 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 Composite on which the filter controls are created. This is used to set
* the background color of the filter controls to match the surrounding
* controls.
*/
protected Composite filterComposite;

/**
* The text to initially show in the filter text control.
*/
protected String initialText = ""; //$NON-NLS-1$

/**
* The job used to refresh the tree.
*/
private Job refreshJob;

/**
* The parent composite of the filtered tree.
*/
protected Composite parent;

/**
* Whether or not to show the filter controls (text and clear button). The
* default is to show these controls. This can be overridden by providing a
* setting in the product configuration file. The setting to add to not show
* these controls is:
*
* org.eclipse.ui/SHOW_FILTERED_TEXTS=false
*/
protected boolean showFilterControls;

/**
* Tells whether this filtered tree is used to make quick selections. In this
* mode the first match in the tree is automatically selected while filtering
* and the 'Enter' key is not used to move the focus to the tree.
*/
private boolean quickSelectionMode = false;

/**
* Time for refresh job delay in terms of expansion in long value
*/
private final long refreshJobDelayInMillis;

protected AbstractFilteredViewer(Composite parent, int style, long refreshJobDelayInMillis) {
super(parent, style);
this.parent = parent;
this.refreshJobDelayInMillis = refreshJobDelayInMillis;
}

/**
* Create the viewer.
*
* @param style the style bits for the {@code viewer}.
*/
protected final void init(int style) {
showFilterControls = PlatformUI.getPreferenceStore()
.getBoolean(IWorkbenchPreferenceConstants.SHOW_FILTERED_TEXTS);
createControl(parent, style);
createRefreshJob();
setInitialText(WorkbenchMessages.FilteredTree_FilterMessage);
setFont(parent.getFont());
getViewer().getControl().addDisposeListener(e -> refreshJob.cancel());
}

/**
* Create the filtered tree's controls. Subclasses should override.
*
* @param parent the parent
* @param treeStyle SWT style bits used to create the tree
*/
protected void createControl(Composite parent, int treeStyle) {
GridLayout layout = new GridLayout();
layout.marginHeight = 0;
layout.marginWidth = 0;
setLayout(layout);

if (parent.getLayout() instanceof GridLayout) {
setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true));
}

if (showFilterControls) {
filterComposite = new Composite(this, SWT.NONE);
GridLayout filterLayout = new GridLayout();
filterLayout.marginHeight = 0;
filterLayout.marginWidth = 0;
filterComposite.setLayout(filterLayout);
filterComposite.setFont(parent.getFont());

createFilterControls(filterComposite);
filterComposite.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;
}

/**
* Create the refresh job for the receiver.
*/
private void createRefreshJob() {
refreshJob = doCreateRefreshJob();
refreshJob.setSystem(true);
}

/**
* 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
*/
protected abstract WorkbenchJob doCreateRefreshJob();

/**
* 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.addFocusListener(new FocusAdapter() {
@Override
public void focusGained(FocusEvent e) {
/*
* Running in an asyncExec because the selectAll() does not appear to work when
* using mouse to give focus to text.
*/
Display display = filterText.getDisplay();
display.asyncExec(() -> {
if (!filterText.isDisposed()) {
if (getInitialText().equals(filterText.getText().trim())) {
filterText.selectAll();
}
}
});
}

@Override
public void focusLost(FocusEvent e) {
if (filterText.getText().equals(initialText)) {
setFilterText(""); //$NON-NLS-1$
textChanged();
}
}
});

filterText.addMouseListener(new MouseAdapter() {
@Override
public void mouseDown(MouseEvent e) {
if (filterText.getText().equals(initialText)) {
// XXX: We cannot call clearText() due to
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=260664
setFilterText(""); //$NON-NLS-1$
textChanged();
}
}
});

filterText.addModifyListener(e -> textChanged());

GridData gridData = new GridData(SWT.FILL, SWT.CENTER, true, false);
filterText.setLayoutData(gridData);
}

/**
* Creates the text control for entering the filter text. Subclasses may
* override.
*
* @param parent the parent composite
* @return the text widget
*/
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() {
// 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 refreshJobDelayInMillis;
}

/**
* Clears the text in the filter text widget.
*/
protected void clearText() {
setFilterText(""); //$NON-NLS-1$
textChanged();
}

/**
* Set the text in the filter control.
*
* @param filterText the text to set.
*/
protected void setFilterText(String filterText) {
if (this.filterText != null) {
this.filterText.setText(filterText);
selectAll();
}
}

/**
* Get the column viewer of the receiver.
*
* @return the column viewer
*/
public abstract ColumnViewer getViewer();

/**
* 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();
}
}

/**
* Sets whether this filtered tree is used to make quick selections. In this
* mode the first match in the tree is automatically selected while filtering
* and the 'Enter' key is not used to move the focus to the tree.
* <p>
* By default, this is set to <code>false</code>.
* </p>
*
* @param enabled <code>true</code> if this filtered tree is used to make quick
* selections, <code>false</code> otherwise
*/
public void setQuickSelectionMode(boolean enabled) {
this.quickSelectionMode = enabled;
}

/**
* 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;
}

/**
* Get the quick selection mode.
*
* @return {@code true}, if enabled.
*/
protected boolean isQuickSelectionMode() {
return quickSelectionMode;
}
}
Loading

0 comments on commit 5ccbcda

Please sign in to comment.