From 61ce53040766d939d008e49ab879b2fddf2adcec Mon Sep 17 00:00:00 2001 From: Patrick Ziegler Date: Thu, 23 Jan 2025 20:15:51 +0100 Subject: [PATCH] Implement filter-based TableViewer as a counterpart to the FilteredTree Projects such as PDE currently use the FilteredTree in combination with a child-less ITreeContentProvider to simulate a filtered table. This approach brings a big performance overhead, as the tree still has to handle functionality such as expansion and contraction of the (flat) tree structure which especially large trees/tables causes a significant problems. --- .../.settings/.api_filters | 11 + .../dialogs/filteredtree/FilteredTable.java | 465 ++++++++++++++++++ .../dialogs/filteredtree/PatternFilter.java | 11 +- .../dialogs/textbundles/E4DialogMessages.java | 4 +- .../dialogs/textbundles/messages.properties | 4 +- .../org.eclipse.ui.examples.filter/.classpath | 11 + .../org.eclipse.ui.examples.filter/.project | 34 ++ .../org.eclipse.core.resources.prefs | 2 + .../.settings/org.eclipse.jdt.core.prefs | 11 + .../META-INF/MANIFEST.MF | 13 + .../org.eclipse.ui.examples.filter/README.TXT | 7 + .../org.eclipse.ui.examples.filter/about.html | 36 ++ .../build.properties | 8 + .../plugin.properties | 7 + .../org.eclipse.ui.examples.filter/plugin.xml | 26 + .../ui/examples/filter/FilteredTableView.java | 62 +++ .../ui/examples/filter/FilteredTreeView.java | 84 ++++ .../filter/FilteredVirtualTableView.java | 75 +++ .../eclipse/ui/examples/filter/Messages.java | 30 ++ .../ui/examples/filter/messages.properties | 3 + .../org/eclipse/ui/tests/UiTestSuite.java | 2 + .../filteredtree/FilteredTableTests.java | 130 +++++ .../org.eclipse.ui.tests/META-INF/MANIFEST.MF | 1 + 23 files changed, 1030 insertions(+), 7 deletions(-) create mode 100644 bundles/org.eclipse.e4.ui.dialogs/.settings/.api_filters create mode 100644 bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/filteredtree/FilteredTable.java create mode 100644 examples/org.eclipse.ui.examples.filter/.classpath create mode 100644 examples/org.eclipse.ui.examples.filter/.project create mode 100644 examples/org.eclipse.ui.examples.filter/.settings/org.eclipse.core.resources.prefs create mode 100644 examples/org.eclipse.ui.examples.filter/.settings/org.eclipse.jdt.core.prefs create mode 100644 examples/org.eclipse.ui.examples.filter/META-INF/MANIFEST.MF create mode 100644 examples/org.eclipse.ui.examples.filter/README.TXT create mode 100644 examples/org.eclipse.ui.examples.filter/about.html create mode 100644 examples/org.eclipse.ui.examples.filter/build.properties create mode 100644 examples/org.eclipse.ui.examples.filter/plugin.properties create mode 100644 examples/org.eclipse.ui.examples.filter/plugin.xml create mode 100644 examples/org.eclipse.ui.examples.filter/src/org/eclipse/ui/examples/filter/FilteredTableView.java create mode 100644 examples/org.eclipse.ui.examples.filter/src/org/eclipse/ui/examples/filter/FilteredTreeView.java create mode 100644 examples/org.eclipse.ui.examples.filter/src/org/eclipse/ui/examples/filter/FilteredVirtualTableView.java create mode 100644 examples/org.eclipse.ui.examples.filter/src/org/eclipse/ui/examples/filter/Messages.java create mode 100644 examples/org.eclipse.ui.examples.filter/src/org/eclipse/ui/examples/filter/messages.properties create mode 100644 tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/filteredtree/FilteredTableTests.java diff --git a/bundles/org.eclipse.e4.ui.dialogs/.settings/.api_filters b/bundles/org.eclipse.e4.ui.dialogs/.settings/.api_filters new file mode 100644 index 00000000000..928a29c4bcf --- /dev/null +++ b/bundles/org.eclipse.e4.ui.dialogs/.settings/.api_filters @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/filteredtree/FilteredTable.java b/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/filteredtree/FilteredTable.java new file mode 100644 index 00000000000..e39a38e92d6 --- /dev/null +++ b/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/filteredtree/FilteredTable.java @@ -0,0 +1,465 @@ +/******************************************************************************* + * Copyright (c) 2025 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.e4.ui.dialogs.filteredtree; + +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.e4.ui.dialogs.textbundles.E4DialogMessages; +import org.eclipse.jface.resource.JFaceResources; +import org.eclipse.jface.viewers.AbstractFilteredViewerComposite; +import org.eclipse.jface.viewers.IContentProvider; +import org.eclipse.jface.viewers.ISelection; +import org.eclipse.jface.viewers.TableViewer; +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.KeyAdapter; +import org.eclipse.swt.events.KeyEvent; +import org.eclipse.swt.graphics.Color; +import org.eclipse.swt.graphics.Font; +import org.eclipse.swt.layout.GridData; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.layout.RowData; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Display; +import org.eclipse.swt.widgets.Table; +import org.eclipse.swt.widgets.TableItem; +import org.eclipse.swt.widgets.Text; + +/** + * A simple control that provides a text widget and a table viewer. The contents + * of the text widget are used to drive a TextMatcher that is on the viewer. + * + * @since 1.6 + */ +public class FilteredTable extends AbstractFilteredViewerComposite { + + /** + * The viewer for the filtered table. This value should never be {@code null} + * after the widget creation methods are complete. + */ + private TableViewer tableViewer; + + /** + * The job used to refresh the table. + */ + private Job refreshJob; + + private Composite tableComposite; + + /** + * Default time for refresh job delay in ms + */ + private static final long DEFAULT_REFRESH_TIME = 200; + + /** + * Create a new instance of the receiver. + * + * @param parent the parent {@code Composite} + * @param tableStyle the style bits for the {@code table} + * @param filter the filter to be used + * @param refreshDelayTime refresh delay in ms, the time to expand the table + * after debounce + */ + public FilteredTable(Composite parent, int tableStyle, PatternFilter filter, long refreshDelayTime) { + super(parent, SWT.NONE, refreshDelayTime); + this.parent = getParent(); + init(tableStyle, filter); + } + + /** + * Calls {@link #FilteredTable(Composite, int, PatternFilter, long)} with a + * default refresh time + */ + public FilteredTable(Composite parent, int tableStyle, PatternFilter filter) { + this(parent, tableStyle, filter, DEFAULT_REFRESH_TIME); + } + + /** + * Create a new instance of the receiver. Subclasses that wish to override + * the default creation behavior may use this constructor, but must ensure + * that the init(composite, int, PatternFilter) method is + * called in the overriding constructor. + * + * @param parent + * the parent Composite + * @see #init(int, PatternFilter) + */ + protected FilteredTable(Composite parent) { + super(parent, SWT.NONE, DEFAULT_REFRESH_TIME); + } + + @Override + protected void init(int tableStyle, PatternFilter filter) { + setShowFilterControls(true); + super.init(tableStyle, filter); + createRefreshJob(); + setInitialText(E4DialogMessages.FilteredTable_FilterMessage); + } + + @Override + protected void createControl(Composite parent, int tableStyle) { + super.createControl(parent, tableStyle); + + tableComposite = new Composite(this, SWT.NONE); + GridLayout tableCompositeLayout = new GridLayout(); + tableCompositeLayout.marginHeight = 0; + tableCompositeLayout.marginWidth = 0; + tableComposite.setLayout(tableCompositeLayout); + GridData data = new GridData(SWT.FILL, SWT.FILL, true, true); + tableComposite.setLayoutData(data); + createTableControl(tableComposite, tableStyle); + } + + /** + * Creates and set up the table and table viewer. This method calls + * {@link #doCreateTableViewer(Composite, int)} to create the table viewer. + * Subclasses should override {@link #doCreateTableViewer(Composite, int)} + * instead of overriding this method. + * + * @param parent parent {@code Composite} + * @param style SWT style bits used to create the table + * @return the table + */ + protected Control createTableControl(Composite parent, int style) { + tableViewer = doCreateTableViewer(parent, style); + tableViewer.setUseHashlookup(true); + GridData data = new GridData(SWT.FILL, SWT.FILL, true, true); + tableViewer.getControl().setLayoutData(data); + tableViewer.getControl().addDisposeListener(e -> refreshJob.cancel()); + if (tableViewer instanceof NotifyingTableViewer) { + getPatternFilter().setUseCache(true); + } + tableViewer.addFilter(getPatternFilter()); + return tableViewer.getControl(); + } + + /** + * Creates the table viewer. Subclasses may override. + * + * @param parent the parent composite + * @param style SWT style bits used to create the table viewer + * @return the table viewer + */ + protected TableViewer doCreateTableViewer(Composite parent, int style) { + return new NotifyingTableViewer(parent, style); + } + + /** + * Return the first item in the table that matches the filter pattern. + * + * @return the first matching TableItem + */ + private TableItem getFirstMatchingItem(TableItem[] items) { + for (TableItem item : items) { + if (getPatternFilter().isLeafMatch(tableViewer, item.getData()) + && getPatternFilter().isElementSelectable(item.getData())) { + return item; + } + } + return null; + } + + /** + * Create the refresh job for the receiver. + */ + private void createRefreshJob() { + refreshJob = doCreateRefreshJob(); + refreshJob.setSystem(true); + } + + /** + * Creates a workbench job that will refresh the table based on the current + * filter text. Subclasses may override. + * + * @return a job that can be scheduled to refresh the table + */ + protected Job doCreateRefreshJob() { + return new BasicUIJob("Refresh Filter", getDisplay()) {//$NON-NLS-1$ + @Override + public IStatus runInUIThread(IProgressMonitor monitor) { + if (tableViewer.getControl().isDisposed()) { + return Status.CANCEL_STATUS; + } + + String text = getFilterString(); + if (text == null) { + return Status.OK_STATUS; + } + + boolean initial = initialText != null && initialText.equals(text); + if (initial) { + getPatternFilter().setPattern(null); + } else if (text != null) { + getPatternFilter().setPattern(text); + } + + tableViewer.refresh(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 Composite of the filter text + */ + @Override + protected void createFilterText(Composite parent) { + super.createFilterText(parent); + filterText.getAccessible().addAccessibleListener(new AccessibleAdapter() { + @Override + public void getName(AccessibleEvent e) { + String filterTextString = filterText.getText(); + if (filterTextString.isEmpty() || filterTextString.equals(initialText)) { + e.result = initialText; + } else { + e.result = NLS.bind(E4DialogMessages.FilteredTable_AccessibleListenerFiltered, + new String[] { filterTextString, String.valueOf(getFilteredItemsCount()) }); + } + } + + /** + * Return the number of filtered items + * + * @return int + */ + private int getFilteredItemsCount() { + return getViewer().getTable().getItems().length; + } + }); + + filterText.addKeyListener(new KeyAdapter() { + @Override + public void keyPressed(KeyEvent e) { + // on a CR we want to transfer focus to the list + boolean hasItems = getViewer().getTable().getItemCount() > 0; + if (hasItems && e.keyCode == SWT.ARROW_DOWN) { + tableViewer.getTable().setFocus(); + return; + } + } + }); + + // enter key set focus to table + filterText.addTraverseListener(e -> { + if (e.detail == SWT.TRAVERSE_RETURN) { + e.doit = false; + if (getViewer().getTable().getItemCount() == 0) { + Display.getCurrent().beep(); + } else { + // if the initial filter text hasn't changed, do not try + // to match + boolean hasFocus = getViewer().getTable().setFocus(); + boolean textChanged = !getInitialText().equals(filterText.getText().trim()); + if (hasFocus && textChanged && filterText.getText().trim().length() > 0) { + Table table = getViewer().getTable(); + TableItem item; + if (table.getSelectionCount() > 0) { + item = getFirstMatchingItem(table.getSelection()); + } else { + item = getFirstMatchingItem(table.getItems()); + } + if (item != null) { + table.setSelection(new TableItem[] { item }); + ISelection sel = getViewer().getSelection(); + getViewer().setSelection(sel, true); + } + } + } + } + }); + } + + @Override + protected Text doCreateFilterText(Composite parent) { + return new Text(parent, SWT.SINGLE | SWT.BORDER | SWT.SEARCH | SWT.ICON_CANCEL | SWT.ICON_SEARCH); + } + + @Override + protected void textChanged() { + // cancel currently running job first, to prevent unnecessary redraw + refreshJob.cancel(); + refreshJob.schedule(getRefreshJobDelay()); + } + + /** + * Set the background for the widgets that support the filter text area. + * + * @param background background Color to set + */ + @Override + public void setBackground(Color background) { + super.setBackground(background); + if (filterComposite != null) { + filterComposite.setBackground(background); + } + } + + @Override + public final PatternFilter getPatternFilter() { + return (PatternFilter) super.getPatternFilter(); + } + + @Override + public TableViewer getViewer() { + return tableViewer; + } + + /** + * Return a bold font if the given element matches the given pattern. Clients + * can opt to call this method from a Viewer's label provider to get a bold font + * for which to highlight the given element in the table. + * + * @param element element for which a match should be determined + * @param table FilteredTable in which the element resides + * @param filter PatternFilter which determines a match + * + * @return bold font + */ + public static Font getBoldFont(Object element, FilteredTable table, PatternFilter filter) { + String filterText = table.getFilterString(); + + if (filterText == null) { + return null; + } + + // Do nothing if it's empty string + String initialText = table.getInitialText(); + if (!filterText.isEmpty() && !filterText.equals(initialText)) { + if (table.getPatternFilter() != filter) { + boolean initial = initialText != null && initialText.equals(filterText); + if (initial) { + filter.setPattern(null); + } else if (filterText != null) { + filter.setPattern(filterText); + } + } + if (filter.isElementVisible(table.getViewer(), element) && filter.isLeafMatch(table.getViewer(), element)) { + return JFaceResources.getFontRegistry().getBold(JFaceResources.DIALOG_FONT); + } + } + return null; + } + + public boolean isShowFilterControls() { + return showFilterControls; + } + + public void setShowFilterControls(boolean showFilterControls) { + this.showFilterControls = showFilterControls; + if (filterComposite != null) { + Object filterCompositeLayoutData = filterComposite.getLayoutData(); + if (filterCompositeLayoutData instanceof GridData) { + ((GridData) filterCompositeLayoutData).exclude = !isShowFilterControls(); + } else if (filterCompositeLayoutData instanceof RowData) { + ((RowData) filterCompositeLayoutData).exclude = !isShowFilterControls(); + } + filterComposite.setVisible(isShowFilterControls()); + layout(); + } + } + + /** + * Custom table viewer subclass that clears the caches in patternFilter on any + * change to the table. See bug 187200. + */ + class NotifyingTableViewer extends TableViewer { + + public NotifyingTableViewer(Composite parent, int style) { + super(parent, style); + } + + @Override + public void add(Object element) { + getPatternFilter().clearCaches(); + super.add(element); + } + + @Override + public void add(Object[] elements) { + getPatternFilter().clearCaches(); + super.add(elements); + } + + @Override + protected void inputChanged(Object input, Object oldInput) { + getPatternFilter().clearCaches(); + super.inputChanged(input, oldInput); + } + + @Override + public void insert(Object element, int position) { + getPatternFilter().clearCaches(); + super.insert(element, position); + } + + @Override + public void refresh() { + getPatternFilter().clearCaches(); + super.refresh(); + } + + @Override + public void refresh(boolean updateLabels) { + getPatternFilter().clearCaches(); + super.refresh(updateLabels); + } + + @Override + public void refresh(Object element) { + getPatternFilter().clearCaches(); + super.refresh(element); + } + + @Override + public void refresh(Object element, boolean updateLabels) { + getPatternFilter().clearCaches(); + super.refresh(element, updateLabels); + } + + @Override + public void remove(Object element) { + getPatternFilter().clearCaches(); + super.remove(element); + } + + @Override + public void remove(Object[] elements) { + getPatternFilter().clearCaches(); + super.remove(elements); + } + + @Override + public void replace(Object element, int index) { + getPatternFilter().clearCaches(); + super.replace(element, index); + } + + @Override + public void setContentProvider(IContentProvider provider) { + getPatternFilter().clearCaches(); + super.setContentProvider(provider); + } + } +} diff --git a/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/filteredtree/PatternFilter.java b/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/filteredtree/PatternFilter.java index a1d2113beaa..377f52aad1b 100644 --- a/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/filteredtree/PatternFilter.java +++ b/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/filteredtree/PatternFilter.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2004, 2015 IBM Corporation and others. + * Copyright (c) 2004, 2025 IBM Corporation and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -274,11 +274,12 @@ public boolean isElementVisible(Viewer viewer, Object element) { * text */ protected boolean isParentMatch(Viewer viewer, Object element) { - Object[] children = ((ITreeContentProvider) ((AbstractTreeViewer) viewer) - .getContentProvider()).getChildren(element); + if (viewer instanceof AbstractTreeViewer + && ((AbstractTreeViewer) viewer).getContentProvider() instanceof ITreeContentProvider) { + Object[] children = ((ITreeContentProvider) ((AbstractTreeViewer) viewer).getContentProvider()) + .getChildren(element); - if ((children != null) && (children.length > 0)) { - return isAnyVisible(viewer, element, children); + return children != null && children.length > 0 && isAnyVisible(viewer, element, children); } return false; } diff --git a/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/textbundles/E4DialogMessages.java b/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/textbundles/E4DialogMessages.java index 1acd9474aab..b0d0c1aa817 100644 --- a/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/textbundles/E4DialogMessages.java +++ b/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/textbundles/E4DialogMessages.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2014 vogella GmbH and others. + * Copyright (c) 2014, 2025 vogella GmbH and others. * * This program and the accompanying materials * are made available under the terms of the Eclipse Public License 2.0 @@ -21,6 +21,8 @@ public class E4DialogMessages extends NLS { private static final String BUNDLE_NAME = "org.eclipse.e4.ui.dialogs.textbundles.messages";//$NON-NLS-1$ + public static String FilteredTable_AccessibleListenerFiltered; + public static String FilteredTable_FilterMessage; public static String FilteredTree_AccessibleListenerClearButton; public static String FilteredTree_ClearToolTip; public static String FilteredTree_FilterMessage; diff --git a/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/textbundles/messages.properties b/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/textbundles/messages.properties index 2a5a5d1a2e8..aa50092befa 100644 --- a/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/textbundles/messages.properties +++ b/bundles/org.eclipse.e4.ui.dialogs/src/org/eclipse/e4/ui/dialogs/textbundles/messages.properties @@ -1,5 +1,5 @@ ############################################################################### -# Copyright (c) 2000, 2014 IBM Corporation and others. +# Copyright (c) 2000, 2025 IBM Corporation and others. # # This program and the accompanying materials # are made available under the terms of the Eclipse Public License 2.0 @@ -19,6 +19,8 @@ # Based on /org.eclipse.ui.workbench/Eclipse UI/org/eclipse/ui/internal/messages.properties +FilteredTable_AccessibleListenerFiltered={0} {1} matches. +FilteredTable_FilterMessage=type filter text FilteredTree_AccessibleListenerClearButton=Clear filter field FilteredTree_ClearToolTip=Clear FilteredTree_FilterMessage=type filter text diff --git a/examples/org.eclipse.ui.examples.filter/.classpath b/examples/org.eclipse.ui.examples.filter/.classpath new file mode 100644 index 00000000000..c5932f42c7e --- /dev/null +++ b/examples/org.eclipse.ui.examples.filter/.classpath @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/examples/org.eclipse.ui.examples.filter/.project b/examples/org.eclipse.ui.examples.filter/.project new file mode 100644 index 00000000000..7186350c037 --- /dev/null +++ b/examples/org.eclipse.ui.examples.filter/.project @@ -0,0 +1,34 @@ + + + org.eclipse.ui.examples.filter + + + + + + org.eclipse.jdt.core.javabuilder + + + + + org.eclipse.pde.ManifestBuilder + + + + + org.eclipse.pde.SchemaBuilder + + + + + org.eclipse.m2e.core.maven2Builder + + + + + + org.eclipse.jdt.core.javanature + org.eclipse.pde.PluginNature + org.eclipse.m2e.core.maven2Nature + + diff --git a/examples/org.eclipse.ui.examples.filter/.settings/org.eclipse.core.resources.prefs b/examples/org.eclipse.ui.examples.filter/.settings/org.eclipse.core.resources.prefs new file mode 100644 index 00000000000..99f26c0203a --- /dev/null +++ b/examples/org.eclipse.ui.examples.filter/.settings/org.eclipse.core.resources.prefs @@ -0,0 +1,2 @@ +eclipse.preferences.version=1 +encoding/=UTF-8 diff --git a/examples/org.eclipse.ui.examples.filter/.settings/org.eclipse.jdt.core.prefs b/examples/org.eclipse.ui.examples.filter/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 00000000000..3d1fdb619b3 --- /dev/null +++ b/examples/org.eclipse.ui.examples.filter/.settings/org.eclipse.jdt.core.prefs @@ -0,0 +1,11 @@ +eclipse.preferences.version=1 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=17 +org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve +org.eclipse.jdt.core.compiler.compliance=17 +org.eclipse.jdt.core.compiler.debug.lineNumber=generate +org.eclipse.jdt.core.compiler.debug.localVariable=generate +org.eclipse.jdt.core.compiler.debug.sourceFile=generate +org.eclipse.jdt.core.compiler.problem.enablePreviewFeatures=disabled +org.eclipse.jdt.core.compiler.problem.reportPreviewFeatures=warning +org.eclipse.jdt.core.compiler.release=enabled +org.eclipse.jdt.core.compiler.source=17 diff --git a/examples/org.eclipse.ui.examples.filter/META-INF/MANIFEST.MF b/examples/org.eclipse.ui.examples.filter/META-INF/MANIFEST.MF new file mode 100644 index 00000000000..21b83e2758a --- /dev/null +++ b/examples/org.eclipse.ui.examples.filter/META-INF/MANIFEST.MF @@ -0,0 +1,13 @@ +Manifest-Version: 1.0 +Bundle-ManifestVersion: 2 +Bundle-Name: %Bundle-Name +Bundle-SymbolicName: org.eclipse.ui.examples.filter;singleton:=true +Bundle-Vendor: %Bundle-Vendor +Bundle-Version: 1.0.0.qualifier +Require-Bundle: org.eclipse.ui;bundle-version="3.207.0", + org.eclipse.core.runtime;bundle-version="3.32.100", + org.eclipse.e4.ui.dialogs;bundle-version="1.6.0" +Bundle-RequiredExecutionEnvironment: JavaSE-17 +Automatic-Module-Name: org.eclipse.ui.examples.filter +Bundle-Localization: plugin +Bundle-ActivationPolicy: lazy diff --git a/examples/org.eclipse.ui.examples.filter/README.TXT b/examples/org.eclipse.ui.examples.filter/README.TXT new file mode 100644 index 00000000000..a641f19b8c0 --- /dev/null +++ b/examples/org.eclipse.ui.examples.filter/README.TXT @@ -0,0 +1,7 @@ +This examples plug-in demonstrates the following features: + +- Creating a tree viewer with a simple filter +- Creating a table viewer with a simple filter +- Creating a lazy table viewer with a simple filter + +The examples can be seen by opening the "Filter Tree View", "Filter Table View" and "Filter (Virtual) Table View, respectively. \ No newline at end of file diff --git a/examples/org.eclipse.ui.examples.filter/about.html b/examples/org.eclipse.ui.examples.filter/about.html new file mode 100644 index 00000000000..164f781a8fd --- /dev/null +++ b/examples/org.eclipse.ui.examples.filter/about.html @@ -0,0 +1,36 @@ + + + + +About + + +

About This Content

+ +

November 30, 2017

+

License

+ +

+ The Eclipse Foundation makes available all content in this plug-in + ("Content"). Unless otherwise indicated below, the Content + is provided to you under the terms and conditions of the Eclipse + Public License Version 2.0 ("EPL"). A copy of the EPL is + available at http://www.eclipse.org/legal/epl-2.0. + For purposes of the EPL, "Program" will mean the Content. +

+ +

+ If you did not receive this Content directly from the Eclipse + Foundation, the Content is being redistributed by another party + ("Redistributor") and different terms and conditions may + apply to your use of any object code in the Content. Check the + Redistributor's license that was provided with the Content. If no such + license exists, contact the Redistributor. Unless otherwise indicated + below, the terms and conditions of the EPL still apply to any source + code in the Content and such source code may be obtained at http://www.eclipse.org. +

+ + + \ No newline at end of file diff --git a/examples/org.eclipse.ui.examples.filter/build.properties b/examples/org.eclipse.ui.examples.filter/build.properties new file mode 100644 index 00000000000..adac29f50d5 --- /dev/null +++ b/examples/org.eclipse.ui.examples.filter/build.properties @@ -0,0 +1,8 @@ +source.. = src/ +output.. = bin/ +bin.includes = META-INF/,\ + plugin.properties,\ + plugin.xml,\ + about.html,\ + . +src.includes = about.html diff --git a/examples/org.eclipse.ui.examples.filter/plugin.properties b/examples/org.eclipse.ui.examples.filter/plugin.properties new file mode 100644 index 00000000000..9fa8ad8205c --- /dev/null +++ b/examples/org.eclipse.ui.examples.filter/plugin.properties @@ -0,0 +1,7 @@ +#Properties file for org.eclipse.ui.examples.filter +Bundle-Vendor = Eclipse.org +Bundle-Name = Eclipse Filtered Viewer + +view.tree.name = Filtered Tree View +view.table.name = Filtered Table View +view.table.virtual.name = Filtered (Virtual) Table View \ No newline at end of file diff --git a/examples/org.eclipse.ui.examples.filter/plugin.xml b/examples/org.eclipse.ui.examples.filter/plugin.xml new file mode 100644 index 00000000000..451dd217b11 --- /dev/null +++ b/examples/org.eclipse.ui.examples.filter/plugin.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + diff --git a/examples/org.eclipse.ui.examples.filter/src/org/eclipse/ui/examples/filter/FilteredTableView.java b/examples/org.eclipse.ui.examples.filter/src/org/eclipse/ui/examples/filter/FilteredTableView.java new file mode 100644 index 00000000000..3576f441c02 --- /dev/null +++ b/examples/org.eclipse.ui.examples.filter/src/org/eclipse/ui/examples/filter/FilteredTableView.java @@ -0,0 +1,62 @@ +/******************************************************************************* + * Copyright (c) 2025 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.examples.filter; + +import java.util.ArrayList; +import java.util.List; + +import org.eclipse.e4.ui.dialogs.filteredtree.FilteredTable; +import org.eclipse.e4.ui.dialogs.filteredtree.PatternFilter; +import org.eclipse.jface.viewers.ArrayContentProvider; +import org.eclipse.jface.viewers.IContentProvider; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.part.ViewPart; + +public class FilteredTableView extends ViewPart { + protected FilteredTable filter; + protected TableViewer viewer; + protected List input; + + @Override + public void createPartControl(Composite parent) { + input = generateInput(9); + filter = createFilteredTable(parent); + viewer = filter.getViewer(); + viewer.setContentProvider(createContentProvider()); + viewer.setInput(input); + } + + @Override + public void setFocus() { + viewer.getControl().setFocus(); + } + + protected FilteredTable createFilteredTable(Composite parent) { + return new FilteredTable(parent, SWT.NONE, new PatternFilter(), 500); + } + + protected IContentProvider createContentProvider() { + return ArrayContentProvider.getInstance(); + } + + private List generateInput(int size) { + List input = new ArrayList<>(); + for (int i = 1; i <= size; ++i) { + input.add(Messages.bind(Messages.FilteredTableView_Element, i)); + } + return input; + } +} diff --git a/examples/org.eclipse.ui.examples.filter/src/org/eclipse/ui/examples/filter/FilteredTreeView.java b/examples/org.eclipse.ui.examples.filter/src/org/eclipse/ui/examples/filter/FilteredTreeView.java new file mode 100644 index 00000000000..b186e45c556 --- /dev/null +++ b/examples/org.eclipse.ui.examples.filter/src/org/eclipse/ui/examples/filter/FilteredTreeView.java @@ -0,0 +1,84 @@ +/******************************************************************************* + * Copyright (c) 2025 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.examples.filter; + +import java.util.Map; +import java.util.TreeMap; + +import org.eclipse.jface.viewers.ITreeContentProvider; +import org.eclipse.jface.viewers.TreeViewer; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.ui.dialogs.FilteredTree; +import org.eclipse.ui.dialogs.PatternFilter; +import org.eclipse.ui.part.ViewPart; + +public class FilteredTreeView extends ViewPart { + private FilteredTree filter; + private TreeViewer viewer; + + @Override + public void createPartControl(Composite parent) { + filter = new FilteredTree(parent, SWT.NONE, new PatternFilter(), true, true, 500); + viewer = filter.getViewer(); + viewer.setContentProvider(new TreeContentProvider(9)); + viewer.setInput(new Object()); + viewer.expandAll(); + } + + @Override + public void setFocus() { + viewer.getControl().setFocus(); + } + + private static class TreeContentProvider implements ITreeContentProvider { + private Map elements = new TreeMap<>(); + + public TreeContentProvider(int size) { + for (int i = 1; i <= size; ++i) { + String key = Messages.bind(Messages.FilteredTreeView_Parent, i); + String value = Messages.bind(Messages.FilteredTreeView_Child, i); + elements.put(key, value); + } + } + + @Override + public Object[] getElements(Object inputElement) { + return elements.keySet().toArray(); + } + + @Override + public Object[] getChildren(Object parentElement) { + if (!hasChildren(parentElement)) { + return new Object[0]; + } + return new Object[] { elements.get(parentElement) }; + } + + @Override + public Object getParent(Object element) { + for (Map.Entry entry : elements.entrySet()) { + if (entry.getValue().equals(element)) { + return entry.getKey(); + } + } + return null; + } + + @Override + public boolean hasChildren(Object element) { + return elements.containsKey(element); + } +} +} diff --git a/examples/org.eclipse.ui.examples.filter/src/org/eclipse/ui/examples/filter/FilteredVirtualTableView.java b/examples/org.eclipse.ui.examples.filter/src/org/eclipse/ui/examples/filter/FilteredVirtualTableView.java new file mode 100644 index 00000000000..d2e80399280 --- /dev/null +++ b/examples/org.eclipse.ui.examples.filter/src/org/eclipse/ui/examples/filter/FilteredVirtualTableView.java @@ -0,0 +1,75 @@ +/******************************************************************************* + * Copyright (c) 2025 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.examples.filter; + +import java.util.Arrays; +import java.util.List; + +import org.eclipse.core.runtime.jobs.IJobChangeEvent; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.core.runtime.jobs.JobChangeAdapter; +import org.eclipse.e4.ui.dialogs.filteredtree.FilteredTable; +import org.eclipse.e4.ui.dialogs.filteredtree.PatternFilter; +import org.eclipse.jface.viewers.IContentProvider; +import org.eclipse.jface.viewers.ILazyContentProvider; +import org.eclipse.jface.viewers.TableViewer; +import org.eclipse.jface.viewers.Viewer; +import org.eclipse.jface.viewers.ViewerFilter; +import org.eclipse.swt.SWT; +import org.eclipse.swt.widgets.Composite; + +public class FilteredVirtualTableView extends FilteredTableView { + protected FilteredTable createFilteredTable(Composite parent) { + return new FilteredTable(parent, SWT.VIRTUAL, new PatternFilter(), 500) { + @Override + protected Job doCreateRefreshJob() { + Job job = super.doCreateRefreshJob(); + job.addJobChangeListener(new JobChangeAdapter() { + @Override + public void done(IJobChangeEvent event) { + if (viewer.getControl().isDisposed()) { + return; + } + ViewerFilter filter = viewer.getFilters()[0]; + Object[] newInput = filter.filter(viewer, (Object) null, input.toArray()); + viewer.setInput(Arrays.asList(newInput)); + } + }); + return job; + } + }; + } + + protected IContentProvider createContentProvider() { + return new ContentProvider(); + } + + private static class ContentProvider implements ILazyContentProvider { + private List input; + private TableViewer viewer; + + @Override + public void updateElement(int index) { + viewer.replace(input.get(index), index); + } + + @Override + @SuppressWarnings("unchecked") + public void inputChanged(Viewer viewer, Object oldInput, Object newInput) { + this.input = (List) newInput; + this.viewer = (TableViewer) viewer; + this.viewer.setItemCount(input == null ? 0 : input.size()); + } + } +} diff --git a/examples/org.eclipse.ui.examples.filter/src/org/eclipse/ui/examples/filter/Messages.java b/examples/org.eclipse.ui.examples.filter/src/org/eclipse/ui/examples/filter/Messages.java new file mode 100644 index 00000000000..d8a039e1741 --- /dev/null +++ b/examples/org.eclipse.ui.examples.filter/src/org/eclipse/ui/examples/filter/Messages.java @@ -0,0 +1,30 @@ +/******************************************************************************* + * Copyright (c) 2025 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.examples.filter; + +import org.eclipse.osgi.util.NLS; + +public class Messages extends NLS { + private static final String BUNDLE_NAME = Messages.class.getPackageName() + ".messages"; //$NON-NLS-1$ + static { + NLS.initializeMessages(BUNDLE_NAME, Messages.class); + } + + public static String FilteredTableView_Element; + public static String FilteredTreeView_Child; + public static String FilteredTreeView_Parent; + + private Messages() { + } +} diff --git a/examples/org.eclipse.ui.examples.filter/src/org/eclipse/ui/examples/filter/messages.properties b/examples/org.eclipse.ui.examples.filter/src/org/eclipse/ui/examples/filter/messages.properties new file mode 100644 index 00000000000..7b03719c9e1 --- /dev/null +++ b/examples/org.eclipse.ui.examples.filter/src/org/eclipse/ui/examples/filter/messages.properties @@ -0,0 +1,3 @@ +FilteredTableView_Element=Element {0} +FilteredTreeView_Child=Child {0} +FilteredTreeView_Parent=Parent {0} diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/UiTestSuite.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/UiTestSuite.java index 008255bf985..66338056e30 100644 --- a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/UiTestSuite.java +++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/UiTestSuite.java @@ -35,6 +35,7 @@ import org.eclipse.ui.tests.dynamicplugins.DynamicPluginsTestSuite; import org.eclipse.ui.tests.encoding.EncodingTestSuite; import org.eclipse.ui.tests.fieldassist.FieldAssistTestSuite; +import org.eclipse.ui.tests.filteredtree.FilteredTableTests; import org.eclipse.ui.tests.filteredtree.FilteredTreeTests; import org.eclipse.ui.tests.filteredtree.PatternFilterTest; import org.eclipse.ui.tests.internal.InternalTestSuite; @@ -88,6 +89,7 @@ CommandsTestSuite.class, ContextsTestSuite.class, ConcurrencyTestSuite.class, + FilteredTableTests.class, FilteredTreeTests.class, PatternFilterTest.class, StatusHandlingTestSuite.class, diff --git a/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/filteredtree/FilteredTableTests.java b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/filteredtree/FilteredTableTests.java new file mode 100644 index 00000000000..a9c3a0a653e --- /dev/null +++ b/tests/org.eclipse.ui.tests/Eclipse UI Tests/org/eclipse/ui/tests/filteredtree/FilteredTableTests.java @@ -0,0 +1,130 @@ +/******************************************************************************* + * Copyright (c) 2025 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.tests.filteredtree; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; + +import org.eclipse.core.runtime.jobs.IJobChangeEvent; +import org.eclipse.core.runtime.jobs.Job; +import org.eclipse.core.runtime.jobs.JobChangeAdapter; +import org.eclipse.e4.ui.dialogs.filteredtree.FilteredTable; +import org.eclipse.e4.ui.dialogs.filteredtree.PatternFilter; +import org.eclipse.jface.dialogs.Dialog; +import org.eclipse.jface.layout.GridDataFactory; +import org.eclipse.jface.tests.viewers.TestElement; +import org.eclipse.jface.tests.viewers.TestModelContentProvider; +import org.eclipse.jface.viewers.LabelProvider; +import org.eclipse.swt.SWT; +import org.eclipse.swt.layout.GridLayout; +import org.eclipse.swt.widgets.Composite; +import org.eclipse.swt.widgets.Control; +import org.eclipse.swt.widgets.Shell; +import org.junit.Before; +import org.junit.Test; + +public class FilteredTableTests { + private final static int NUM_ITEMS = 8000; + private TestFilteredTable tableViewer; + private TestElement rootElement; + + @Before + public void setUp() { + rootElement = TestElement.createModel(1, NUM_ITEMS); + } + + @Test + public void testAddAndRemovePattern() { + Dialog dialog = new FilteredTableDialog(null, SWT.SINGLE | SWT.H_SCROLL | SWT.V_SCROLL | SWT.BORDER); + dialog.create(); + + assertNotNull(tableViewer); + assertEquals(tableViewer.getItemCount(), NUM_ITEMS); + + tableViewer.applyPattern("0-0 name-"); + assertEquals(tableViewer.getItemCount(), 1); + + tableViewer.applyPattern("0-0 name unknownWord"); + assertEquals(tableViewer.getItemCount(), 0); + + tableViewer.applyPattern(""); + assertEquals(tableViewer.getItemCount(), NUM_ITEMS); + + dialog.close(); + } + + private class TestFilteredTable extends FilteredTable { + private boolean jobScheduled; + + public TestFilteredTable(Composite parent, int style) { + super(parent, style, new PatternFilter(), 0); + } + + public int getItemCount() { + return getViewer().getTable().getItemCount(); + } + + public void applyPattern(String pattern) { + setFilterText(pattern); + textChanged(); + + while (jobScheduled) { + getDisplay().readAndDispatch(); + } + } + + @Override + protected Job doCreateRefreshJob() { + Job job = super.doCreateRefreshJob(); + job.addJobChangeListener(new JobChangeAdapter() { + @Override + public void scheduled(IJobChangeEvent event) { + jobScheduled = true; + } + + @Override + public void done(IJobChangeEvent event) { + if (event.getResult().isOK()) { + jobScheduled = false; + } + } + }); + return job; + } + } + + + private class FilteredTableDialog extends Dialog { + private final int style; + + public FilteredTableDialog(Shell shell, int tableStyle) { + super(shell); + style = tableStyle; + } + + @Override + protected Control createContents(Composite parent) { + Composite composite = new Composite(parent, SWT.NONE); + composite.setLayout(new GridLayout()); + + tableViewer = new TestFilteredTable(composite, style); + tableViewer.setLayoutData(GridDataFactory.fillDefaults().hint(400, 500).create()); + tableViewer.getViewer().setContentProvider(new TestModelContentProvider()); + tableViewer.getViewer().setLabelProvider(new LabelProvider()); + tableViewer.getViewer().setUseHashlookup(true); + tableViewer.getViewer().setInput(rootElement); + return parent; + } + } +} diff --git a/tests/org.eclipse.ui.tests/META-INF/MANIFEST.MF b/tests/org.eclipse.ui.tests/META-INF/MANIFEST.MF index ab9288a267d..2e025f6c1bd 100644 --- a/tests/org.eclipse.ui.tests/META-INF/MANIFEST.MF +++ b/tests/org.eclipse.ui.tests/META-INF/MANIFEST.MF @@ -30,6 +30,7 @@ Require-Bundle: org.eclipse.core.resources;bundle-version="3.14.0", org.eclipse.e4.core.di, org.eclipse.e4.ui.workbench.swt;bundle-version="0.9.1", org.eclipse.e4.ui.di;bundle-version="0.9.0", + org.eclipse.e4.ui.dialogs;bundle-version="1.6.0", org.eclipse.e4.ui.css.swt.theme;bundle-version="0.9.0", org.eclipse.e4.ui.bindings;bundle-version="0.9.0", org.eclipse.e4.ui.css.swt;bundle-version="0.9.1",